Repository navigation
Scraper Plugins
Some sites are better served by a downloader that already exists than by mirror_base.py. A plugin
runs that other program for a comic, then takes in what it fetched, so the comic is kept like any other:
loose pages in Uncompressed/<Comic>, archives on the CBZs shelf, chapters, ComicInfo, the Reader,
the schedule, and The Web Page.
Each plugin is a folder in the config folder's plugins/ (config/plugins/<id>/), and usually a git
repository of its own. The folder is named by the id in its plugin.json, and that name is how a comic
names its plugin.
config/plugins/<name>/
plugin.json required: how to run the program and what it leaves behind
plugin.py optional: functions that replace the defaults
dialog.js optional: its own form on the web page (else one is built from its fields)
dialog.css optional: the look of that form
tool/ optional: the program itself, usually a git submodule of its own repository
On The Web Page, Plugins (beside Settings, on Add comics) lists the plugins, with Update and Remove for each. Install from takes a plugin repository's git address, and optionally a branch or tag. Each action is a job in the queue, and git's and pip's output goes to the log.
-
Install clones the repository with its submodules, checks its
plugin.json, names its folder by itsid, and installs the Python packages it asks for. A repository that is not a usable plugin leaves nothing behind. - Update pulls the branch it follows and its submodules, then installs its packages again if what it asks for has changed. A plugin installed at a tag stays at that tag; install it again to move it.
- Remove is refused while any comic still uses the plugin.
Installing runs whatever the repository holds, so the page offers it only when MIRROR_WEB_PASSWORD is
set. To allow it on a page with no password, set MIRROR_ALLOW_PLUGIN_INSTALLS=yes in the container's
environment. The page itself cannot switch this on.
From a shell: run_plugin.py --install <git address> [--ref <branch or tag>] and
run_plugin.py --update <id>.
A plugin can also be put in by hand: clone it (git clone --recurse-submodules) or copy its folder into
config/plugins/. A plugin put in by hand shows up without a restart, but it cannot be updated from the page
unless it is a git clone.
examples/plugins/example/ in this repository is a complete plugin to copy from. Its program draws a
made-up series rather than fetching one, so it can be tried with nothing downloaded: copy the folder into
config/plugins/.
{
"id": "example",
"name": "Example",
"version": 1,
"about": "One line shown beside the Scraper picker.",
"command": ["{python}", "{tool}/scraper.py", "{url}", ["--start", "{last_chapter}"], "{reverse}"],
"output": "chapter-cbz",
"fields": [
{"key": "url", "type": "url", "label": "Series address"},
{"key": "reverse", "type": "flag", "label": "Oldest first", "option": "--reverse"}
]
}-
idnames the plugin's folder, and so the plugin every comic that uses it names. It is letters, digits,.,_and-, and it never changes once comics use it. Installing from a repository needs one. A plugin whose folder has another name is refused, with a message saying to rename the folder. -
installis the Python packages its program needs beyond what the image has:"pip"lists them and"requirements"names a requirements file inside the plugin, either or both (see below). -
needslists the other programs it runs, such as["ffmpeg"]. These are only checked: a plugin needing one the image lacks has its runs fail with a message naming it. -
outputsays what the program leaves in the folder it is run in:-
pages: pictures, possibly in subfolders. Videos (.mp4,.webm,.m4v,.mov,.mkv) and Flash (.swf) are pages too: the folder keeps them as they are, and archives hold a stand-in for each. The same goes for the pages ofchapter-foldersandchapter-cbz. -
chapter-folders: a folder of pictures per chapter, named for the chapter. -
chapter-cbz: an archive (.cbzor.zip) per chapter, named for the chapter.
-
-
fieldsare the comic's settings for this plugin. Each has akey, atype(url,text,countorflag), and optionally alabel, adefault, aplaceholder, ahelpandrequired. Aurlfield is required unless it says otherwise. Its value is the comic's address (settings.url), the one the library shows. -
commandis the program and its arguments, run in the comic's staging folder. Placeholders:-
{python}is the Python to run it with: the plugin's own if it has aninstall, otherwise the updater's. -
{plugin}is the plugin's folder, and{tool}is itstool/folder. -
{staging}is where the program is run. - Any field's
{key}. -
{last_chapter}and{last_page}: where the last run got to, as the defaultrememberkeeps it.
A word that is only a placeholder is left out when that placeholder is empty. So is a group of words given as a list,
["--start", "{last_chapter}"], which leaves nothing on a comic's first run. A flag field puts in itsoptionwhen ticked and nothing otherwise. A placeholder that names nothing is an error, so a typo is caught. -
-
remembers: names that aplugin.pyrememberkeeps, so the command may use them. -
setup: the plugin's own settings, the same for every comic of it, such as an account on the site and its key. They are listed likefields, with one more type,secret, and an optionalenv(see below).
fields are a comic's settings. setup is for what belongs to the plugin as a whole, set once and used by
every comic of it:
"setup": [
{"key": "username", "type": "text", "label": "Account", "env": "SITE_USERNAME"},
{"key": "api_key", "type": "secret", "label": "API key", "env": "SITE_API_KEY"},
{"key": "agent", "type": "text", "label": "User-Agent", "default": "MyPlugin/1.0"}
]-
Where they are set: the Settings button beside the plugin in the Plugins dialog. A
secretis shown masked until Show is pressed; whoever can reach the page can see and change it, as with everything else there. Saving takes effect from the plugin's next run. -
Where they are kept:
config/plugin-setup/<id>.json, readable only by the user the updater runs as. Not in the plugin's own folder, so updating or reinstalling the plugin keeps them. -
How the program gets them: in its environment, each as its
envname, orZCM_and its key in capitals (ZCM_AGENT). A flag is1when ticked; a setting with no value is not set at all. A setting nothing was saved for is handed over as itsdefault. -
How
plugin.pygets them: assettings["setup"], a dict by key, in every function it defines. -
In a command: a non-secret setting can be a placeholder,
{agent}, as a field can. Asecretcannot: the command is written into the log and the comic's run record, so a plugin.json that names one is refused with a message saying which environment variable to read instead.
Each of these is optional and replaces the default of the same name:
| Function | Default |
|---|---|
build_command(settings, state, staging) |
Fill in plugin.json's command. |
chapter_order(labels) |
Chapter names sorted with numbers as numbers, so Chapter 9 comes before Chapter 10. |
remember(state, taken) |
Keep the last chapter and the last page taken in, as last_chapter and last_page. taken is {"pages", "chapters", "skipped"}. |
ended(settings, state, taken) |
None: a comic is only ever marked ended by hand. |
ended is for a plugin whose site says when a series is finished. It is asked after every run whose program
finished (never one that failed or ran out of time) of a comic not already ended, with the comic's settings,
the state remember just kept, and what the run took in. It answers True (complete), False (not
complete) or anything else (no answer):
-
Trueticks the comic's ended box, so later updates leave it alone, and the metadata notes it inhistory.plugin_ended. -
Unticking it by hand while the plugin still says
Truesticks: the comic is checked every run, and not marked again. -
False, a series that came back, clears that note, so the nextTruemarks the comic ended again. A series can die, come back and die again as often as it likes. -
No answer changes nothing. An
endedthat raises is reported in the run's log and is no answer too, so a check that fails once neither ends a comic nor makes it look as if the series came back.
plugin.py is run only by the process that runs one of the plugin's comics. The web page reads only
plugin.json.
The web page builds a form from fields. A plugin that wants its own puts itself in pluginForms under
its folder's name:
pluginForms["example"] = {
render(container, values, fields) { /* fill container with inputs showing values */ },
read(container) { return { chapters: "3" }; }, // the settings as typed, by key; the updater checks them
};The page loads it the first time the plugin is chosen. If it fails to load, the page falls back to the form
built from fields. Only .js and .css files at the top of the plugin's folder are served, at
/plugins/<name>/, and nothing in tool/.
On The Web Page, Add comics shows a Scraper picker once a plugin is installed. Choosing a plugin puts away the built-in scraper's options and shows the plugin's settings, which are shared by every row. Each row still has its own folder, archive and address. From a shell:
python run_plugin.py --list
python run_plugin.py --add example Uncompressed/MyComic url=https://example.com/comics/MyComic chapters=5Run --add from the library's root, as update_comics.py runs comics. --no-cbz keeps no archives.
The comic's metadata holds settings.scraper (the plugin's name) and settings.plugin (its settings). Its
Edit dialog shows those settings, and hides what only the built-in scraper has: the start page, numbering,
wait time, the chapter list, and working out chapters.
- The plugin's program runs in
<comic>/.plugin-staging/, and its output appears under Running now. - What it wrote is taken in:
- Pages are numbered on from the comic's last.
- A chapter's name goes into each of its pages' names, since every chapter has its own
001.jpg. - A chapter the comic already has is not taken again, so a program that resumes by fetching its last chapter again is fine.
- The very file the comic already has is not taken twice.
- A different picture under a name the comic already uses is taken in as
(2); nothing is ever written over.
- New chapters are added to the comic's chapter list, and each gets one archive the same way any chaptered comic does. A comic with no chapters keeps a single archive.
- A
ComicInfo.xmlthe program wrote is kept with its chapter (or with the comic, forpages) and written into the archives (details). - Where it got to is remembered for the next command.
A scheduled run stops the program a little before the comic's timeout, so whatever it fetched is still taken in and the next run carries on (exit 13). A run that was killed outright leaves its staging folder behind. The next run takes in whatever of it is whole, and drops an archive that will not open.
Plugin comics are never walked or aligned: their page numbers say which file is which. The safety checks
mirror_base makes (exits 8 to 11) do not apply, because the plugin's program decides the order.
A plugin works best with a program that does three things:
-
It writes where it is run, or where it is told. The program is run in the comic's staging folder, and
only what lands there is taken in. A program that writes beside itself needs an option naming the
folder, passed as
{staging}. -
It uses the container's browser if it drives one. It should take the browser and driver from
MIRROR_BROWSER_BINARYandMIRROR_DRIVER_BINARYwhen those are set. Otherwise Selenium goes looking for a Chrome of its own. - It exits non-zero when it fails. A program that always exits 0 makes a broken run look like one with nothing new.
If you wrote the program, change it. If you can't, the plugin's plugin.py can make up the difference:
- Its
build_commandcan run a small script of the plugin's own in place of the program. - That script imports the program, points its output folder at the working folder, and swaps in a browser setup that uses the variables above.
- It then calls the program's
main()and exits with a code that reflects what happened.
This depends on the program's internals, so recheck it whenever the program's submodule is updated.
The image has Python 3.12 with selenium and requests, plus Chromium and its chromedriver (named by
MIRROR_BROWSER_BINARY and MIRROR_DRIVER_BINARY). A program that needs nothing more runs with that, and
its plugin needs no install.
A program that needs more Python packages names them in install:
"install": {"requirements": "tool/requirements.txt", "pip": ["somepackage>=1.2"]}They are installed into a virtual environment in the plugin's own folder, .venv/. It is made with
--system-site-packages, so the image's selenium and requests are still seen. The command is run with that
environment's Python: {python}, or a plugin.py command that starts with sys.executable, which is
swapped for it.
- When: on install and update, and before any run that finds it missing or out of date. Pip's output goes to that run's log.
- A new image: the environment is in the config folder, so it is kept. If the new image brings a newer Python, which a virtual environment cannot outlive, the next run installs the packages again and says why.
- Failure: if they cannot be installed, the run fails (exit 12) with pip's output in its log, and the next run tries again.
Programs other than Python packages, such as ffmpeg, cannot be installed this way: the container does not
run as root, and anything installed with apt would be lost with the image. Name them in needs so a
missing one is reported clearly.
ZornCoMix is licensed under the PolyForm Noncommercial License 1.0.0. Based on a script by AChillVamp; see NOTICE.
Getting started
Running it
Reading
Archives and chapters
Reference