Skip to content

feat(soulvoice): add API definition using NexusPHP JSON API - #1058

Closed
ChrisonSimtian wants to merge 6 commits into
Prowlarr:masterfrom
ChrisonSimtian:add-soulvoice-api
Closed

ChrisonSimtian wants to merge 6 commits into
Prowlarr:masterfrom
ChrisonSimtian:add-soulvoice-api

Conversation

@ChrisonSimtian

Copy link
Copy Markdown

Indexer/Tracker

SoulVoice (聆音Club) — new soulvoice-api definition. The existing HTML soulvoice definition 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.

  • Search — GET api/v1/torrents, response: {type: json}, Authorization: Bearer {{ .Config.apikey }}.
  • Keywords — filter[title]. This is a custom filter registered in TorrentRepository::getList; the whitelist is TorrentRepository::$allowFilters.
  • Sort — sort=-id|-seeders|-size|-times_completed (Spatie-style, direction in the prefix, so no separate order setting).
  • Download — the bearer token is not accepted by download.php (302 with or without the header), so the definition takes a passkey as a second setting and builds download.php?id=…&passkey=….

Two things worth flagging for reviewers:

Freeleech is read from promotion_info, not sp_state. sp_state records only per-torrent promotions. During a site-wide freeleech event it still reads 1 while the effective promotion is free — e.g. a 2019 torrent returns sp_state: 1 alongside promotion_info: {text: Free, down_multiplier: 0}. Parsing sp_state would 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]=2 returned 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 produced api/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.
  • Search returns correct titles, seeders/leechers, sizes and categories; Chinese keywords work (filter[title]=兰香如故 → 80 results).
  • Dates parse correctly — an upload at 2026-09-19 16:50 site time yields 2026-09-19T08:50Z (site runs UTC+8).
  • Freeleech flags parse, verified against a site-wide free event announced for 2026-09-19 → 2026-09-26.
  • Grab returns a valid torrent: 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/torrents answering with the sanctum guard 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

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
@ChrisonSimtian

Copy link
Copy Markdown
Author

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 ;-)

@garfield69 garfield69 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread definitions/v11/soulvoice-api.yml Outdated
Comment thread definitions/v11/soulvoice-api.yml Outdated
Comment thread definitions/v11/soulvoice-api.yml
Comment thread definitions/v11/soulvoice-api.yml Outdated
Comment thread definitions/v11/soulvoice-api.yml Outdated
@garfield69

Copy link
Copy Markdown
Contributor

Oh, and by the way, Thank you for your contribution ;-)

@ChrisonSimtian

Copy link
Copy Markdown
Author

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.

good question, NexusPHP is open source.
There is an API documentation here but its all chinese.
I told Claude to use the github project as a source of documentation to base this code on and the API documentation above provides a llm.txt, so I guess it will have adhered to the documentation as best as it can.

There also seems to be some english documentation available but thats more nexus itself :-(
https://doc.nexusphp.org/en/

@ChrisonSimtian

Copy link
Copy Markdown
Author

Oh, and by the way, Thank you for your contribution ;-)

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 ;-)

ChrisonSimtian and others added 3 commits September 21, 2026 19:46
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Co-authored-by: garfield69 <garfieldsixtynine@gmail.com>
@ChrisonSimtian

Copy link
Copy Markdown
Author

It would be nice if we could know if the filters support imdbid lookup for example,

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
@sonarqubecloud

Copy link
Copy Markdown

@garfield69

Copy link
Copy Markdown
Contributor

LGTM
I will port this to Jackett and make a few additional touch ups.
I'll add a freeleech filter (since there is no freeleech search option) and tidy up the error block on the login section.
This pull will get replaced by the Jackett source, which the Prowlarr team will import in due course ;-)
I'll post the commit link when I'm done.

@garfield69 garfield69 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants