LazyLibrarian's config system will accept your settings, nod politely, and then do whatever it wants. Found at least four ways this happens. No errors, no warnings. I discovered this by docker exec-ing into the container and reading the Python source. There may be more.
The setup
LazyLibrarian manages ebook discovery and downloads. It talks to Jackett for indexer searches and Deluge for torrent downloads; both run as separate Docker containers on my Unraid server. The config lives in a standard INI file at /config/config.ini.
Everything looked correct. The API responded. Indexers were configured. But when LazyLibrarian found a book match and tried to download it:
WARNING: No torrent download method is enabled, check config.
The config clearly had tor_downloader_deluge = True in a [TORRENT] section. I restarted the container. Same error. I checked the file. The entire [TORRENT] section had vanished.
ConfigBool needs 1, not True
LazyLibrarian's ConfigBool type extends ConfigInt, and the INI parser uses Python's configparser.getint() to read values:
# configtypes.py: ConfigInt.update_from_parser
def update_from_parser(self, parser, tmpsection, name):
try:
value = parser.getint(tmpsection, name, fallback=0)
except Exception:
value = 0 # silent fallback
return self.set_int(value)
getint() parses "1" and "0" fine. It throws ValueError on "True" or "False". The exception is caught, the value defaults to 0, and nothing is logged. Your boolean setting is silently ignored.
# WRONG: silently ignored, no error
tor_downloader_deluge = True
# CORRECT
tor_downloader_deluge = 1
Every boolean in LazyLibrarian works this way. The configdefs.py file defines them all as ConfigBool('SECTION', 'KEY', 0).
Config.ini is rewritten on every shutdown
This explains why the [TORRENT] section kept disappearing.
LazyLibrarian saves its in-memory config to disk on shutdown. Not just cmd=shutdown via the API, also on SIGTERM from docker stop, and on graceful container stop. The save logic drops any section where all values equal their defaults.
Since True failed to parse (see above), the in-memory value stayed at 0, the default. On shutdown, LazyLibrarian saw a section with nothing but defaults and removed it entirely.
The cycle:
- You edit config.ini with
tor_downloader_deluge = True - LazyLibrarian starts, tries
getint("True"), fails silently, uses0 - Container stops, config saves,
[TORRENT]section dropped (all defaults) - You check the file, your edit is gone
Spent twenty minutes wondering why my config kept vanishing before realising the container was eating it every time it stopped. The fix: use 1 instead of True, and the value loads correctly, persists in memory, and survives the rewrite.
The readCFG / writeCFG API
LazyLibrarian has API endpoints for reading and writing individual config values at runtime, no restart required:
# Read a config value
curl "http://HOST:5299/api?cmd=readCFG&name=TOR_DOWNLOADER_DELUGE&group=TORRENT&apikey=$KEY"
# Returns: [1]
# Write a config value (takes effect immediately)
curl "http://HOST:5299/api?cmd=writeCFG&name=TOR_DOWNLOADER_DELUGE&group=TORRENT&value=1&apikey=$KEY"
# Returns: OK
These are safe. They change a single value without side effects. The key names are uppercase (matching configdefs.py), and the group is the INI section name.
This matters because the alternative (the /config_update web endpoint) is dangerous.
The config_update trap
LazyLibrarian's web UI settings page submits ALL config values at once via /config_update. The handler iterates every ConfigBool in the system and does this:
# webServe.py: config_update method
for key, itm in CONFIG.config.items():
if key.lower() in kwargs:
CONFIG.set_from_ui(key, value)
else:
if isinstance(itm, ConfigBool) and itm.get_read_count() > 0:
itm.set_from_ui(False) # resets to False!
If you POST a single parameter to config_update, every boolean that was ever read during the current session gets reset to False. The web UI avoids this by submitting everything. Anyone calling the endpoint programmatically with partial data gets a broken config.
Use writeCFG instead. Always.
The dual API key system
While reading the source, I also found that LazyLibrarian has two separate API keys:
API_KEY: full read-write accessAPI_RO_KEY: read-only access
Write commands like searchBook, addAuthor, and queueBook require the full-access key. If you manually set api_key in config.ini, it might not be recognised as the full-access key. The fix is to generate one via the built-in endpoint:
# Generate a new full-access key
curl "http://HOST:5299/generate_api"
# Generate a read-only key
curl "http://HOST:5299/generate_api?ro=true"
The validation logic in api.py checks incoming keys against both:
# api.py line 381
if kwargs['apikey'] != CONFIG.get_str('API_KEY') and \
kwargs['apikey'] != CONFIG.get_str('API_RO_KEY'):
# reject: invalid key
# api.py line 396: for write commands:
if cmd_dict[cmd][0] != 0 and self.apikey != CONFIG.get_str('API_KEY'):
# reject: read-only key can't execute write commands
The source files
For anyone else who needs to debug LazyLibrarian config issues, here's where to look inside the Docker container:
| File | What it does |
|------|-------------|
| lazylibrarian/configtypes.py | ConfigBool, ConfigInt, update_from_parser |
| lazylibrarian/config2.py | load_configfile, section loading, save logic |
| lazylibrarian/configdefs.py | All config key definitions |
| lazylibrarian/webServe.py | config_update (line ~2021), Deluge test (line ~8460) |
| lazylibrarian/api.py | API key validation, command access control |
| lazylibrarian/downloadmethods.py | Torrent download method checks (line ~975) |
Access them with docker exec lazylibrarian cat /app/lazylibrarian/lazylibrarian/FILENAME.
readCFG and writeCFG are listed in the API docs. The rest I found in the source.