{"id":51572309,"url":"https://github.com/ds17f/watchseries-sonarr","last_synced_at":"2026-07-10T20:31:47.965Z","repository":{"id":359885182,"uuid":"1247764627","full_name":"ds17f/watchseries-sonarr","owner":"ds17f","description":"watchseries.bar scraper + Torznab indexer + fake-qBittorrent download client for Sonarr/Radarr.","archived":false,"fork":false,"pushed_at":"2026-05-23T23:59:27.000Z","size":110,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T00:23:31.841Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ds17f.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T18:46:09.000Z","updated_at":"2026-05-23T23:59:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ds17f/watchseries-sonarr","commit_stats":null,"previous_names":["ds17f/watchseries-sonarr"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ds17f/watchseries-sonarr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ds17f%2Fwatchseries-sonarr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ds17f%2Fwatchseries-sonarr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ds17f%2Fwatchseries-sonarr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ds17f%2Fwatchseries-sonarr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ds17f","download_url":"https://codeload.github.com/ds17f/watchseries-sonarr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ds17f%2Fwatchseries-sonarr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35343135,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-10T20:31:47.774Z","updated_at":"2026-07-10T20:31:47.950Z","avatar_url":"https://github.com/ds17f.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# watchseries-sonarr\n\n\u003e Use `watchseries.bar` as a normal indexer + download client for Sonarr and\n\u003e Radarr — or as a standalone CLI to grab a show.\n\nTwo things in one repo:\n\n1. **`download.py`** — a CLI that takes a `watchseries.bar` URL and writes\n   MP4s to disk. Useful by itself.\n2. **A small service** that pretends to be a [Torznab](https://torznab.github.io/spec-1.3-draft/)\n   indexer **and** a qBittorrent v2 download client. Sonarr/Radarr/Prowlarr\n   treat it like any other indexer + torrent client; under the hood it\n   scrapes `watchseries.bar` via [api.videasy.net](https://www.videasy.net/),\n   decrypts the source list with a bundled WASM module, and `ffmpeg`s the\n   HLS stream to MP4.\n\nSource extraction is vendored from\n[walterwhite-69/Videasy.net-Decryptor](https://github.com/walterwhite-69/Videasy.net-Decryptor) (MIT).\n\n## Why both pieces\n\nThe CLI is the smallest possible thing: \"I want this show, run a command,\nget files.\" The service exists for when you want to keep doing that without\nrunning commands by hand — Sonarr's normal search/grab workflow does it for\nyou, with renaming, library import, and notifications.\n\nYou can use one without the other. They share `src/watchseries/scraper.py`\nso any improvements benefit both.\n\n---\n\n## Use case 1 — CLI\n\n### Setup\n\n```sh\ngit clone https://github.com/ds17f/watchseries-sonarr.git\ncd watchseries-sonarr\nnpm install\npip install fastapi 'uvicorn[standard]' python-multipart   # service deps; CLI uses stdlib only\n```\n\nSystem deps: Python 3.10+, Node.js 18+, `ffmpeg` on PATH.\n\n### Run\n\n```sh\n# Whole series — walks seasons until none more are available.\npython3 download.py \"https://watchseries.bar/tv/men-of-a-certain-age/16208\"\n\n# Single season at lower quality, custom output dir.\npython3 download.py URL --season 1 --end-season 1 --quality 720p --out /mnt/media\n\n# Single episode.\npython3 download.py URL --season 2 --episode 5 --end-season 2\n\n# Movie.\npython3 download.py \"https://watchseries.bar/movie/\u003cslug\u003e/\u003ctmdb_id\u003e\"\n```\n\nDefault output: `~/Downloads/watchseries/\u003cShow Title\u003e/Season NN/\u003cTitle\u003e - sNNeMM.mp4`.\n\nAlready-downloaded episodes (\u003e1MB) are skipped on re-run, so it's safe to\nresume after an interruption.\n\nFlags:\n\n| Flag           | Default     | Meaning |\n|----------------|-------------|---------|\n| `--season N`   | `1`         | start at season N |\n| `--episode N`  | `1`         | start at episode N (within the start season) |\n| `--end-season N` | none      | stop after season N |\n| `--quality Q`  | `1080p`     | preferred quality (`1080p`/`720p`/`360p`); falls back if unavailable |\n| `--out DIR`    | `~/Downloads/watchseries` | output root |\n\n---\n\n## Use case 2 — service wired into Sonarr / Radarr\n\n### What it gives you\n\n- An indexer at `http://\u003chost\u003e:8765/torznab/api` that responds to\n  `t=caps`, `t=tvsearch`, `t=movie`, `t=search` — i.e. exactly what\n  Prowlarr expects from a \"Generic Torznab\" entry.\n- A qBittorrent-compatible API at `http://\u003chost\u003e:8765/api/v2/...` that\n  satisfies Sonarr's \"qBittorrent\" download client.\n\nThe two routers live in one FastAPI process (`src/watchseries/main.py`), so\nit's one container, one port, one log.\n\n### Run with Docker\n\n```sh\ngit clone https://github.com/ds17f/watchseries-sonarr.git\ncd watchseries-sonarr\ncp .env.example .env\n$EDITOR .env    # at minimum set DOWNLOADS_DIR to where you want files\n\ndocker compose up -d --build\ncurl http://localhost:8765/health\n# {\"status\":\"ok\",\"missing\":[],\"download_dir\":\"/downloads\",\"jobs\":0}\n```\n\n### Configuration via `.env`\n\n| Variable             | Default                | Purpose |\n|----------------------|------------------------|---------|\n| `DOWNLOADS_DIR`      | `./downloads`          | Host path bind-mounted as `/downloads` inside the container. Point at Sonarr's existing download folder so import works without remote-path mapping. |\n| `HOST_PORT`          | `8765`                 | Host port to expose the service on. |\n| `PUID` / `PGID`      | `1000` / `1000`        | User/group the container runs as. Must own the `DOWNLOADS_DIR`. (Common NAS setups use 1000/100.) |\n| `TZ`                 | `UTC`                  | Timezone (for log timestamps + scheduling). |\n| `WSG_INDEXER_TITLE`  | `WatchSeries.bar`      | Display name in the Torznab capabilities. |\n| `TMDB_API_KEY`       | _(unset)_              | Optional. If set, the indexer can resolve `tvdbid`/`imdbid` and do text search. Without it, callers must pass `tmdbid` directly. Get a key at https://www.themoviedb.org/settings/api. |\n\n### Wire it into Sonarr\n\n#### 1. Add the indexer (in Prowlarr, recommended)\n\nProwlarr → Indexers → Add → **Generic Torznab**:\n\n- **URL:** `http://watchseries-grabber:8765/torznab` (use the container name if\n  Prowlarr is in the same Docker network; otherwise the host + port)\n- **API Path:** `/api`\n- **API Key:** any non-empty string (we don't enforce one)\n- **Categories:** 5000 (TV), 2040 (Movies/HD)\n\nThen sync to Sonarr / Radarr. You can also add it directly in\nSonarr → Settings → Indexers → Add → Torznab if you don't use Prowlarr.\n\n#### 2. Add the download client\n\nSonarr → Settings → Download Clients → Add → **qBittorrent**:\n\n- **Host:** `watchseries-grabber` (Docker name) or your host's IP\n- **Port:** `8765` (or whatever `HOST_PORT` you set)\n- **URL Base:** _(leave blank)_\n- **Username:** `watchseries`\n- **Password:** anything\n- **Category:** `tv-watchseries` (Sonarr) / `movies-watchseries` (Radarr)\n- **Use SSL:** off\n\nAuthentication is a no-op; we accept any creds and any login attempt\nsucceeds.\n\n#### 3. Path mapping\n\nInside the container files are written to `/downloads`, which is the\n`DOWNLOADS_DIR` host path you set in `.env`. If that same host path is\nmounted into your Sonarr container as `/downloads` too (the typical setup),\nSonarr's import works with no Remote Path Mapping.\n\nIf Sonarr sees the same files under a different path, configure\nSonarr → Settings → Download Clients → Remote Path Mappings.\n\n### Routing specific shows through this indexer\n\nThe cleanest way to \"make Sonarr auto-grab show X from WatchSeries and ignore\ntorrent indexers for it\": **indexer tags**.\n\n1. **Create a tag.** Sonarr → Settings → Tags → `+` → e.g. `watchseries`.\n   (Tags are auto-created the first time you assign one, too.)\n2. **Tag the indexer.** Sonarr → Settings → Indexers → WatchSeries → Tags:\n   `watchseries`. Same in Radarr if you use it for movies.\n3. **Tag each show you want routed here.** Sonarr → Series → Edit → Tags:\n   `watchseries`. Bulk-edit works too.\n\nOnce both sides share the tag, Sonarr's behaviour becomes:\n\n- **Tagged series → only the matching indexers search it.** WatchSeries\n  is the only source for those shows; RSS sync (~15 min) auto-grabs new\n  monitored episodes; interactive search returns only WatchSeries results.\n- **Untagged series → only untagged indexers search it.** Other shows keep\n  using torrents as normal; WatchSeries never sees them.\n\nThis is preferred over a Custom-Format-by-release-group approach because:\n\n- No score fights — a higher-scored torrent won't beat the WatchSeries\n  release just because it's a higher quality tier you don't actually care\n  about for that show.\n- Search load is per-show. We don't get hammered by RSS queries for shows\n  that aren't on watchseries.bar anyway.\n- Clean per-show opt-in, no global preferences to maintain.\n\nIf you instead want WatchSeries as a *fallback* (only used when nothing\nelse returns), no tag config needed — just lower its **Indexer Priority**\n(Sonarr → Indexer → Priority field, lower number = higher priority, so set\nWatchSeries to 50). Sonarr considers all indexers but breaks ties by\npriority and falls back to lower-priority sources when higher ones return\nnothing.\n\n### Caveats\n\n- **`TMDB_API_KEY` strongly recommended.** Without it, only ID-based searches\n  resolve — Sonarr usually passes `tvdbid` for shows, which means we need\n  to look up the TMDB id, which needs the key.\n- **One upstream provider currently.** We use `mb-flix` (one of ~12\n  endpoints exposed by `api.videasy.net`). If it stops working, swap the\n  `API` constant in `src/watchseries/scraper.py`. Known alternates:\n  `cdn`, `moviebox`, `1movies`, `m4uhd`, `hdmovie`, `lamovie`, `superflix`,\n  `meine`.\n- **No real torrent.** The `urn:btih:` info-hash in our magnets is a\n  deterministic SHA-1 of the request payload, not a real torrent hash. Don't\n  put us behind a VPN that blocks non-torrent traffic on these \"magnet\"\n  URIs — they're plain HTTP under the hood.\n\n### How does it work?\n\nWhen Sonarr searches the indexer, we return RSS items with magnets like:\n\n```\nmagnet:?xt=urn:btih:\u003csha1(payload)\u003e\n       \u0026dn=\u003crelease.name\u003e\n       \u0026xs=watchseries://tv/\u003ctmdb_id\u003e/s\u003cN\u003e/e\u003cM\u003e?quality=1080p\u0026title=...\u0026year=...\n```\n\nThe `xs=` parameter carries the real payload — media type, TMDB id,\nseason/episode, clean title (needed for the upstream API match), year.\nSonarr forwards this magnet to its download client; our fake qBittorrent\nunwraps it and starts a job. The job runs the same pipeline as the CLI:\n\n```\napi.videasy.net/mb-flix → hex ciphertext\nnode decrypt.js          → JSON {sources: [{url, quality}], subtitles: [...]}\nffmpeg                   → out.mp4 in /downloads/\u003crelease.name\u003e/Season NN/\n```\n\nWhile downloading, the job reports `state: downloading` with a `progress`\nfraction computed from ffmpeg's stderr; when done it reports `state:\npausedUP` with `content_path` pointing at the folder Sonarr should import.\n\n---\n\n## Layout\n\n```\n.\n├── decrypt.js              # WASM bridge (vendored, MIT)\n├── module.wasm\n├── package.json            # crypto-js, hashids\n├── requirements.txt        # fastapi, uvicorn, python-multipart\n├── Dockerfile\n├── docker-compose.yml\n├── docker-compose.homelab.yml   # external network override (optional)\n├── .env.example\n├── download.py             # CLI entry point\n└── src/watchseries/\n    ├── scraper.py          # videasy fetch + decrypt + ffmpeg\n    ├── jobs.py             # JobManager + magnet codec\n    ├── torznab.py          # /torznab/api router\n    ├── fakeqbt.py          # /api/v2 router (qBittorrent mimic)\n    └── main.py             # FastAPI app\n```\n\n## Legal\n\nThis scrapes a piracy aggregator. Where you are matters. Use accordingly.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fds17f%2Fwatchseries-sonarr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fds17f%2Fwatchseries-sonarr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fds17f%2Fwatchseries-sonarr/lists"}