Tool for backing up tagged items from Inoreader using the official API and local JSON backups.
Inoreader’s API refers to both folders and tags as “labels,” but this toolset relies on a naming convention to tell them apart:
- Folders are Title Case (e.g.
Programming,Travel,Linux Stuff). - Tags are lowercase (e.g.
cpp,linux-stuff,games to play).
Only lowercase labels are treated as “exportable” tags for backup and clearing:
ino_run_batch.pyusestag/listand filters to labels whose names are entirely lowercase.- Those lowercase labels are the ones processed by
ino_process_tag.py, merged byino_merge_outputs.py, and optionally cleared byino_clear.py.
If you follow the same convention (folders Title Case, tags lowercase), the tools will avoid touching your folder structure and will only operate on your tag-style labels.
-
Install Python (3.11+ recommended) and optionally create/activate a virtual env.
-
Install dependencies (managed by
uv):uv sync
-
Create
.envfrom the example:copy .env.example .env # Windows # or cp .env.example .env # macOS/Linux
-
Run the setup helper to configure OAuth:
uv run ino_setup.py
-
First run (no
.envyet) does full setup:- Prompts for
INOREADER_CLIENT_ID,INOREADER_CLIENT_SECRET,INOREADER_REDIRECT_URI. - Prompts for
INOREADER_SCOPEwith:Scope [Enter=keep, r=read, rw=readwrite, custom=type value]: - Prints an auth URL, you open it, authorize, and paste back the
code. - Saves
INOREADER_ACCESS_TOKENandINOREADER_REFRESH_TOKENinto.env.
- Prompts for
-
Later, to refresh tokens:
uv run ino_setup.py
This uses the existing
.envand refresh token to get a new access token and update.env. - To force a clean full setup (e.g. new app or redirect URI):uv run ino_setup.py --full
-
-
Ensure your
.envscope allows tag editing. For batch label clears, you need Zone 2 access, for example:INOREADER_SCOPE="read write"so that
edit-tagoperations are permitted.
Inoreader enforces strict daily limits per client app:
- Zone 1 (read operations like
stream/contents,tag/list): 100 requests per day. - Zone 2 (write operations like
edit-tag): 100 requests per day.
This tool is designed to:
- Use Zone 1 sparingly, fetching up to 100 items per
stream/contentscall (Inoreader’s maximumn) and stopping early when the daily Zone 1 cap is reached. - Use Zone 2 aggressively by batching many article IDs into each
edit-tagcall:- Inoreader’s
edit-tagAPI lets you pass theiparameter multiple times (or as an array) to apply the same add/remove tags to multiple items at once. - In practice, we’ve verified that a single
edit-tagcall can handle thousands of item IDs (e.g., 1k–5k IDs in one request) and still only counts as one Zone 2 request.
- Inoreader’s
The effective pattern is:
- Use
stream/contents(Zone 1) to walk each label once per day, up to the 100‑request limit, writing JSON snapshots and tracking item IDs instate/state.json. - Use batched
edit-tag(Zone 2) to clear labels from those items in as few requests as possible, often clearing thousands of items per day with just a handful of Zone 2 calls.
When the daily Zone 1 limit is exhausted (HTTP 429 “Daily request limit reached!”), the batch driver exits cleanly and you can still run ino_clear.py to perform Zone 2-only cleanup using IDs already stored in state/state.json.
Fetch items for a single Inoreader label via stream/contents and write per-run JSON snapshots.
Input
- Env vars from
.env:INOREADER_ACCESS_TOKEN(used at runtime).
- Label name (e.g.
travel,android-stuff).
Behavior
- Calls Inoreader’s
stream/contents/user/-/label/<label>API and pages up to 100 items per request (Inoreader’s max) until:- The stream ends,
max_itemsis reached, or- Zone 1 quota is nearly exhausted (based on rate-limit headers).
- Writes each run to:
output/<label>_<unix_timestamp>.json(immutable per-run snapshot).
- Updates
state/state.jsonwithpending_idsfor that label (one ID per item still carrying the label).
Usage
uv run --env-file .env ino_process_tag.py travel
uv run --env-file .env ino_process_tag.py travel --max-items 5000Turn per-run outputs into backups for a label and keep backups tidy.
Input
- Per-run outputs for a label:
output/<label>_*.json
- Existing backups (if any):
backup/<label>_*.jsonbackup/<label>.json
Behavior per run for a label
- Batch backup from outputs, then clear outputs
- Merge all
output/<label>_*.jsoninto a dict keyed byid(deduped). - Write a dated batch backup:
backup/<label>_<YYYY-MM-DDTHH-MM-SSZ>.json
- Delete the merged
output/<label>_*.jsonfiles.
- Merge all
- Full backup from existing full + all dated
- Start from existing
backup/<label>.jsonif present. - Merge in all
backup/<label>_*.json. - Dedupe by
id. - Write updated full backup:
backup/<label>.json(always “everything so far” for that label).
- Start from existing
Usage
uv run --env-file .env ino_merge_outputs.py travelAfter running:
backup/travel_*.json= batch snapshots.backup/travel.json= full backup fortravel.
Clear labels from items using the IDs tracked in state/state.json, using batched edit-tag calls in Zone 2.
Input
state/state.jsonwith per-labelpending_ids.- Label name (e.g.
travel,cpp). - Optional CLI flags to control batching.
Behavior
- For the given label, reads
pending_idsand calls Inoreader’sedit-tagto remove that label from those items, moving IDs intodone_idsinstate/state.json. - Uses a single
edit-tagcall with manyiparameters per batch, so thousands of items can be cleared in just a few Zone 2 requests (batches of up to ~5k IDs have been confirmed to work). - Does not perform any Zone 1 (read) calls; it only uses stored IDs and the rate-limit info returned by
edit-tag. This means you can keep usingino_clear.pyeven after the daily Zone 1 limit is exhausted, as long as Zone 2 still has quota.
Usage
# Clear a specific label with large batched Zone 2 calls
uv run --env-file .env ino_clear.py travel --batch-size 5000
# Cap the number of edit-tag calls for this run
uv run --env-file .env ino_clear.py cpp --batch-size 5000 --max-calls 5
# See how many calls a given batch size would use, without hitting the API
uv run --env-file .env ino_clear.py cpp --batch-size 5000 --dry-run
# Show per-label pending/done counts from state/state.json
uv run --env-file .env ino_clear.py --summaryFlags:
--batch-size: Maximum item IDs peredit-tagcall (default: 5000).--max-calls: Optional cap on the number ofedit-tagcalls for this run (useful to respect the 100‑requests/day Zone 2 limit).--dry-run: Print what would be done (batches and call counts) without calling the API or modifying state.--summary: Do not clear anything; just print per‑labelpending/donecounts fromstate/state.json.
Usage
# Daily backup for all lowercase “exportable” labels
uv run --env-file .env ino_run_batch.py
# Daily backup + post-backup label clearing (Zone2 batched)
uv run --env-file .env ino_run_batch.py --clear-afterWith --clear-after, a single daily run can:
- Walk each exportable label via
stream/contentsup to the 100‑requests/day Zone 1 limit. - Maintain deduped JSON backups per label on disk.
- Clear thousands of labeled items per label in just a few Zone 2 calls, thanks to batched
edit-tag.
Single entry point for Inoreader OAuth:
- Full setup when
.envis missing or--fullis passed. - Refresh access token when
.envalready exists.
See Setup section above for details and usage.
Low-level helpers around the Inoreader API:
get_access_token()– readINOREADER_ACCESS_TOKENfrom env.fetch_tags()/list_exportable_labels()– wraptag/listto discover labels.fetch_stream_for_label()– paged fetch fromstream/contentsfor a label (with rate-limit aware stopping).remove_label_from_item()– calledit-tagto remove a label from a single item.remove_label_from_items()– calledit-tagonce to remove a label from many items in a batch (multipleiparameters).RateLimitInfo– wraps Inoreader’s rate-limit headers (X-Reader-Zone1-*,X-Reader-Zone2-*,X-Reader-Limits-Reset-After) and exposes helpers likeremaining_zone1(),remaining_zone2(),can_afford(zone, calls).
State management for label runs (backed by state/state.json):
- Tracks, per label:
pending_ids– items whose label is still applied.done_ids– items where the label has been cleared.
Typical helpers:
load_state()/save_state()– read/writestate/state.json.add_pending_ids(state, label, ids)– add new IDs topending_idswithout duplicates.mark_ids_done(state, label, ids)– move IDs frompending_idstodone_ids.summarize_labels(state)– return simple per-label summary lineslabel: pending=N, done=M(used byino_clear.py --summary).
Used by:
ino_process_tag.pyon fetch (to add pending IDs).ino_clear.pyon clear (to move IDs to done and show summaries).
Small helper to exercise build_auth_url from ino_setup.py with your current .env.
Usage
uv run test_auth_url.pyPrints the auth URL that ino_setup.py uses internally.
Interactive helper for testing token exchange and refresh functions.
Usage
uv run test_token_flow.py- Option 1: test
exchange_code_for_tokenswith a pasted auth code. - Option 2: test
refresh_tokenswith your current refresh token (prints the raw response; normal refresh is handled byino_setup.py).
A common way to use these tools together is:
-
Run the batch driver to fetch and back up items for all “exportable” labels (lowercase names):
uv run --env-file .env ino_run_batch.py
This walks labels via
stream/contents(Zone 1), writes per-run snapshots underoutput/, and updatesstate/state.jsonwithpending_idsper label. -
(Optional) Immediately clear labels after backup in the same run:
uv run --env-file .env ino_run_batch.py --clear-after
With
--clear-after, each processed label is also passed toino_clear.clear_label_from_state, which removes that label from items using batchededit-tagcalls in Zone 2. -
At any later point (even after Zone 1 is exhausted for the day), run targeted clears for individual labels using only Zone 2:
# Clear a single lowercase label based on pending IDs already stored in state.json uv run --env-file .env ino_clear.py cpp --batch-size 5000 --max-calls 5 # Inspect current state without clearing anything uv run --env-file .env ino_clear.py --summary
Because
ino_clear.pyonly uses stored IDs and callsedit-tag(Zone 2), you can keep clearing labels as long as there is Zone 2 quota, even when Zone 1 (read) requests have hit their daily limit.For extra safety, run with
--dry-runfirst to see which lowercase labels and how many items would be affected before making changes.