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:
Programming, Travel, Linux Stuff).cpp, linux-stuff, games to play).Only lowercase labels are treated as “exportable” tags for backup and clearing:
ino_run_batch.py uses tag/list and filters to labels whose names are entirely lowercase.ino_process_tag.py, merged by ino_merge_outputs.py, and optionally cleared by ino_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 dependencies (managed by uv):
uv sync
Create .env from 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
.env yet) does full setup:
INOREADER_CLIENT_ID, INOREADER_CLIENT_SECRET, INOREADER_REDIRECT_URI.INOREADER_SCOPE with: Scope [Enter=keep, r=read, rw=readwrite, custom=type value]:code.INOREADER_ACCESS_TOKEN and INOREADER_REFRESH_TOKEN into .env.Later, to refresh tokens:
uv run ino_setup.py
This uses the existing .env and 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 .env scope allows tag editing. For batch label clears, you need Zone 2 access, for example:
INOREADER_SCOPE="read write"
so that edit-tag operations are permitted.
Inoreader enforces strict daily limits per client app:
stream/contents, tag/list): 100 requests per day.edit-tag): 100 requests per day.This tool is designed to:
stream/contents call (Inoreader’s maximum n) and stopping early when the daily Zone 1 cap is reached.edit-tag call:
edit-tag API lets you pass the i parameter multiple times (or as an array) to apply the same add/remove tags to multiple items at once.edit-tag call can handle thousands of item IDs (e.g., 1k–5k IDs in one request) and still only counts as one Zone 2 request.The effective pattern is:
stream/contents (Zone 1) to walk each label once per day, up to the 100‑request limit, writing JSON snapshots and tracking item IDs in state/state.json.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:
INOREADER_ACCESS_TOKEN (used at runtime).travel, android-stuff).Behavior
stream/contents/user/-/label/<label> API and pages up to 100 items per request (Inoreader’s max) until:
max_items is reached, oroutput/<label>_<unix_timestamp>.json (immutable per-run snapshot).state/state.json with pending_ids for 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 5000
Turn per-run outputs into backups for a label and keep backups tidy.
Input
output/<label>_*.jsonbackup/<label>_*.jsonbackup/<label>.jsonBehavior per run for a label
output/<label>_*.json into a dict keyed by id (deduped).backup/<label>_<YYYY-MM-DDTHH-MM-SSZ>.jsonoutput/<label>_*.json files.backup/<label>.json if present.backup/<label>_*.json.id.backup/<label>.json (always “everything so far” for that label).Usage
uv run --env-file .env ino_merge_outputs.py travel
After running:
backup/travel_*.json = batch snapshots.backup/travel.json = full backup for travel.Clear labels from items using the IDs tracked in state/state.json, using batched edit-tag calls in Zone 2.
Input
state/state.json with per-label pending_ids.travel, cpp).Behavior
pending_ids and calls Inoreader’s edit-tag to remove that label from those items, moving IDs into done_ids in state/state.json.edit-tag call with many i parameters 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).edit-tag. This means you can keep using ino_clear.py even 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 --summary
Flags:
--batch-size: Maximum item IDs per edit-tag call (default: 5000).--max-calls: Optional cap on the number of edit-tag calls 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‑label pending / done counts from state/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-after
With --clear-after, a single daily run can:
stream/contents up to the 100‑requests/day Zone 1 limit.edit-tag.Single entry point for Inoreader OAuth:
.env is missing or --full is passed..env already exists.See Setup section above for details and usage.
Low-level helpers around the Inoreader API:
get_access_token() – read INOREADER_ACCESS_TOKEN from env.fetch_tags() / list_exportable_labels() – wrap tag/list to discover labels.fetch_stream_for_label() – paged fetch from stream/contents for a label (with rate-limit aware stopping).remove_label_from_item() – call edit-tag to remove a label from a single item.remove_label_from_items() – call edit-tag once to remove a label from many items in a batch (multiple i parameters).RateLimitInfo – wraps Inoreader’s rate-limit headers (X-Reader-Zone1-*, X-Reader-Zone2-*, X-Reader-Limits-Reset-After) and exposes helpers like remaining_zone1(), remaining_zone2(), can_afford(zone, calls).State management for label runs (backed by state/state.json):
pending_ids – items whose label is still applied.done_ids – items where the label has been cleared.Typical helpers:
load_state() / save_state() – read/write state/state.json.add_pending_ids(state, label, ids) – add new IDs to pending_ids without duplicates.mark_ids_done(state, label, ids) – move IDs from pending_ids to done_ids.summarize_labels(state) – return simple per-label summary lines label: pending=N, done=M (used by ino_clear.py --summary).Used by:
ino_process_tag.py on fetch (to add pending IDs).ino_clear.py on 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.py
Prints 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
exchange_code_for_tokens with a pasted auth code.refresh_tokens with your current refresh token (prints the raw response; normal refresh is handled by ino_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 under output/, and updates state/state.json with pending_ids per 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 to ino_clear.clear_label_from_state, which removes that label from items using batched edit-tag calls 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.py only uses stored IDs and calls edit-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-run first to see which lowercase labels and how many items would be affected before making changes.