feat(soulvoice): add API definition using NexusPHP JSON API - #1058
ChrisonSimtian wants to merge 6 commits into
Conversation
SoulVoice runs NexusPHP's Laravel API (auth:sanctum), which avoids the image CAPTCHA the HTML definition must solve on every re-login. Search uses GET api/v1/torrents with an Authorization: Bearer header and the filter[title] parameter whitelisted in TorrentRepository. Downloads still require the passkey, since the bearer token is not accepted by download.php, so the definition takes both. Freeleech is read from promotion_info rather than sp_state: sp_state only records per-torrent promotions, so during a site-wide freeleech event it still reads 1 while promotion_info.down_multiplier is 0. The existing HTML definition is left in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmsQRkntrHTc3DLbSrmZS2
|
AI generated but under my supervision. I was a bit annoyed that all the Nexus trackers do a login form auth but then also provide a rest API ... makes it a bit less brittle ;-) |
There was a problem hiding this comment.
I don't suppose you have an url to some form of docs for the api?
It would be nice if we could know if the filters support imdbid lookup for example, as well as filtering by one or more categories, which would bring it in like with the current html scraping indexers.
|
Oh, and by the way, Thank you for your contribution ;-) |
good question, NexusPHP is open source. There also seems to be some english documentation available but thats more nexus itself :-( |
you're welcome :-) Sorry if the quality is not en par, as I said its mostly vibe coded but hopefully it'll be/become good enough to fill this niche ;-) |
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
|
❌ The last analysis has failed. |
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
no doesnt seem so |
The sort query parameter is `sorts`, not `sort`. ApiQueryBuilder declares
PARAM_NAME_SORTS = "sorts" and reads it with request->query('sorts'), so the
previous value was accepted by the site and silently ignored.
Category filtering now works. ApiQueryBuilder::applyFilterOperator implements
`in` as whereIn($field, is_array($value) ? $value : explode(',', $value)), so
repeated filter[category][in][] params are taken as an array.
Verified on a live account: "Grinch" returns 6 results unfiltered and 2 when
restricted to TV categories, with the request carrying
filter[category][in][]=402..405.
Also drops the info_freeleech setting, which explained an absent option to
end users rather than belonging in the UI; the reasoning is now a comment on
the downloadvolumefactor field where a maintainer will see it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmsQRkntrHTc3DLbSrmZS2
|
|
LGTM |



Indexer/Tracker
SoulVoice (聆音Club) — new
soulvoice-apidefinition. The existing HTMLsoulvoicedefinition is left untouched.Description
SoulVoice runs NexusPHP's Laravel API (
auth:sanctum), so it can be driven with a bearer token instead of the login form. The HTML definition has to solve an image CAPTCHA on every re-login, which means Prowlarr cannot re-authenticate unattended — saving the indexer or letting the session lapse produces图片代码无效!("invalid image code") and counts against the site's consecutive-failure IP ban.GET api/v1/torrents,response: {type: json},Authorization: Bearer {{ .Config.apikey }}.filter[title]. This is a custom filter registered inTorrentRepository::getList; the whitelist isTorrentRepository::$allowFilters.sort=-id|-seeders|-size|-times_completed(Spatie-style, direction in the prefix, so no separate order setting).download.php(302 with or without the header), so the definition takes apasskeyas a second setting and buildsdownload.php?id=…&passkey=….Two things worth flagging for reviewers:
Freeleech is read from
promotion_info, notsp_state.sp_staterecords only per-torrent promotions. During a site-wide freeleech event it still reads1while the effective promotion is free — e.g. a 2019 torrent returnssp_state: 1alongsidepromotion_info: {text: Free, down_multiplier: 0}. Parsingsp_statewould silently miss almost every free torrent during exactly the events users care about. For the same reason there is deliberately no "freeleech only" setting:filter[sp_state]=2returned 491 of 118400 torrents during a site-wide free event.URLs are built from
{{ .Config.sitelink }}. Relative paths resolve against the search path (api/v1/), which producedapi/v1/download.php→ 404.Also available from the API and not visible to the HTML definition: per-torrent H&R flag, real file counts, and poster URLs.
Testing
Tested against a live SoulVoice account on Prowlarr via
Definitions/Custom:python3 scripts/validate.py --single definitions/v11/soulvoice-api.yml definitions/v11/schema.json— passes.filter[title]=兰香如故→ 80 results).2026-09-19 16:50site time yields2026-09-19T08:50Z(site runs UTC+8).HTTP 200,application/x-bittorrent,d8:announce79:https://pt.soulvoice.club/….I only hold an account on SoulVoice, so that is the only site this is tested against. Worth noting for anyone extending it: probing the Chinese NexusPHP trackers shows
api/v1/torrentsanswering with thesanctumguard on ZmPT, LemonHD, CrabPT, NicePT, DiscFan and PTSBAO as well, so the same shape may generalise — but I have not verified the filter whitelist or response schema on any of them, and this PR makes no claim about them.Issues Fixed or Closed by this PR
None.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VmsQRkntrHTc3DLbSrmZS2