Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d676ec4
docs: brainstorm spec for public torrent-site catalog
masterdraco Apr 20, 2026
3bed030
docs: implementation plan for public torrent-site catalog
masterdraco Apr 20, 2026
1e3a9e5
chore: ignore .worktrees/ for local worktree workflow
masterdraco Apr 20, 2026
e21ac2b
feat: add catalog_slug field to IndexerRow
masterdraco Apr 20, 2026
8a522e4
feat: migration adds indexer.catalog_slug column
masterdraco Apr 20, 2026
551fc0a
feat: add catalog registry for 12 public torrent sites
masterdraco Apr 20, 2026
d01ab34
feat: catalog service loads registry + vendored YAMLs
masterdraco Apr 20, 2026
e3948db
test: catalog integrity checks (pre-vendor, expected to fail)
masterdraco Apr 20, 2026
8b903b7
style: apply ruff format + drop unused noqa in catalog code
masterdraco Apr 20, 2026
72ad602
feat: script to sync/diff vendored catalog YAMLs with upstream
masterdraco Apr 20, 2026
fda22ba
vendor: import 12 Cardigann YAML definitions from Prowlarr-indexers
masterdraco Apr 20, 2026
582bc2e
fix: align torrentgalaxy mirrors with vendored YAML's actual links
masterdraco Apr 20, 2026
b4c4a3d
docs: fix stale upstream repo name in update-catalog.py docstring
masterdraco Apr 20, 2026
55299de
feat: cardigann urldecode filter
masterdraco Apr 20, 2026
c918a53
feat: cardigann split filter
masterdraco Apr 20, 2026
9b070ba
feat: cardigann trim filter
masterdraco Apr 20, 2026
92f0d82
feat: cardigann tolower filter
masterdraco Apr 20, 2026
edb1c24
feat: cardigann re_replace filter
masterdraco Apr 20, 2026
a624e20
feat: cardigann warns once per unknown filter name
masterdraco Apr 20, 2026
829ff47
style: ruff format
masterdraco Apr 20, 2026
f56c9c9
feat: scaffold catalog API router with GET /catalog
masterdraco Apr 20, 2026
08b176a
feat: POST /api/indexers/catalog/{slug} installs a catalog entry
masterdraco Apr 20, 2026
31e1ef8
test: catalog API endpoints
masterdraco Apr 20, 2026
51a2152
feat(web): catalog types + api.indexers.catalog methods
masterdraco Apr 20, 2026
791827e
feat(web): /indexers/catalog tile grid for catalog entries
masterdraco Apr 20, 2026
f43ac44
feat(web): browse catalog button on /indexers
masterdraco Apr 20, 2026
c81fff1
refactor(web): use reactive reassignment + targeted update on catalog…
masterdraco Apr 20, 2026
23213a9
feat(onboarding): public-sites step between indexer and AI
masterdraco Apr 20, 2026
8c8d534
test: parametrized catalog fixture extraction harness
masterdraco Apr 20, 2026
4bc31ba
docs: document public-site catalog in indexers guide
masterdraco Apr 20, 2026
dff95b9
docs: temper aspirational claim about catalog refresh automation
masterdraco Apr 20, 2026
3c9d514
feat(cardigann): parse settings.default into config_defaults
masterdraco Apr 20, 2026
58b6964
feat(cardigann): minimal Go template expander + wire into search path…
masterdraco Apr 20, 2026
9e4854b
refactor: drop TPB + YTS from catalog (JSON APIs, need native drivers)
masterdraco Apr 20, 2026
7e7441e
test: capture Nyaa search HTML fixture
masterdraco Apr 20, 2026
32d00c6
docs: revise catalog list after dropping TPB and YTS
masterdraco Apr 20, 2026
fefd8da
style: ruff fix unused noqa directives and unused variable in templat…
masterdraco Apr 20, 2026
e90b453
feat(cardigann): support or-expressions and inline re_replace in temp…
masterdraco Apr 20, 2026
369a165
v0.11.0: public torrent-site catalog with Go template support
masterdraco Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ session.secret
# Logs
*.log
/logs/

# Worktrees
.worktrees/
9 changes: 9 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TODO

## Catalog fixtures

- [ ] Capture `<slug>-search.html` fixtures for the 11 remaining catalog slugs.
Pattern: `backend/tests/fixtures/catalog/<slug>-search.html`.
Extraction test lives in `backend/tests/test_catalog_fixtures.py` and
is parametrized over whichever fixtures are present. When captured,
the test runs `_extract_release` against each row and asserts ≥1 result.
38 changes: 38 additions & 0 deletions backend/migrations/versions/0016_indexer_catalog_slug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""indexer.catalog_slug column

Revision ID: 0016
Revises: 0015
Create Date: 2026-04-20

"""

from __future__ import annotations

import sqlalchemy as sa
import sqlmodel
from alembic import op

revision: str = "0016"
down_revision: str | None = "0015"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
with op.batch_alter_table("indexer") as batch:
batch.add_column(
sa.Column(
"catalog_slug",
sqlmodel.sql.sqltypes.AutoString(length=64),
nullable=True,
)
)
op.create_index(
"ix_indexer_catalog_slug", "indexer", ["catalog_slug"], unique=False
)


def downgrade() -> None:
op.drop_index("ix_indexer_catalog_slug", table_name="indexer")
with op.batch_alter_table("indexer") as batch:
batch.drop_column("catalog_slug")
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "trove"
version = "0.10.4"
version = "0.11.0"
description = "A modern replacement for FlexGet with multi-indexer search, Usenet support, and optional local AI."
readme = "../README.md"
requires-python = ">=3.12"
Expand Down
119 changes: 119 additions & 0 deletions backend/src/trove/api/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlmodel import Session, select

from trove.api.deps import current_user, db_session
from trove.api.indexers import IndexerOut, _to_out
from trove.clients.base import Protocol
from trove.indexers.base import Category
from trove.indexers.cardigann import load_definition_yaml
from trove.models.indexer import IndexerRow
from trove.models.user import User
from trove.services import catalog, indexer_registry

router = APIRouter()


class CatalogEntryOut(BaseModel):
slug: str
display_name: str
description: str
categories: list[Category]
mirrors: list[str]
default_mirror: str
protocol: Protocol
logo: str | None = None
already_installed: bool


class CatalogInstallRequest(BaseModel):
base_url: str = Field(min_length=1, max_length=512)
name: str | None = Field(default=None, max_length=64)


@router.get("", response_model=list[CatalogEntryOut])
async def list_catalog(
session: Session = Depends(db_session),
_user: User = Depends(current_user),
) -> list[CatalogEntryOut]:
installed_slugs = set(
session.exec(
select(IndexerRow.catalog_slug).where(
IndexerRow.catalog_slug.is_not(None) # type: ignore[union-attr]
)
).all()
)
out: list[CatalogEntryOut] = []
for entry in catalog.list_entries():
out.append(
CatalogEntryOut(
slug=entry.slug,
display_name=entry.display_name,
description=entry.description,
categories=entry.categories,
mirrors=entry.mirrors,
default_mirror=entry.default_mirror,
protocol=entry.protocol,
logo=entry.logo,
already_installed=entry.slug in installed_slugs,
)
)
return out


def _dedup_name(session: Session, base: str) -> str:
candidate = base
suffix = 2
while session.exec(select(IndexerRow).where(IndexerRow.name == candidate)).first() is not None:
candidate = f"{base}-{suffix}"
suffix += 1
return candidate


@router.post("/{slug}", response_model=IndexerOut, status_code=status.HTTP_201_CREATED)
async def install_catalog_entry(
slug: str,
payload: CatalogInstallRequest,
session: Session = Depends(db_session),
_user: User = Depends(current_user),
) -> IndexerOut:
try:
entry = catalog.get_entry(slug)
except KeyError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="unknown_slug") from None

if payload.base_url not in entry.mirrors:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="base_url_not_in_catalog_mirrors",
)

try:
yaml_text = catalog.read_yaml(slug)
load_definition_yaml(yaml_text)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"catalog_yaml_broken: {e}",
) from e

base_name = payload.name or entry.display_name
name = _dedup_name(session, base_name)

row = IndexerRow(
name=name,
type="cardigann",
protocol=entry.protocol.value,
base_url=payload.base_url,
credentials_cipher=indexer_registry.encrypt_credentials({}),
definition_yaml=yaml_text,
enabled=True,
priority=50,
catalog_slug=slug,
)
session.add(row)
session.commit()
session.refresh(row)
return _to_out(row)
25 changes: 22 additions & 3 deletions backend/src/trove/docs/03-indexers.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,36 @@ For trackers that don't have a native API, Cardigann uses YAML definition files
5. Save and test

**What works**:
- Static `search.paths[0].path`
- `search.rows.selector` (CSS selectors)
- `search.paths[0].path` with Go-template expansion (`{{ .Keywords }}`, `{{ if .Keywords }}…{{ else }}…{{ end }}`, `{{ .Config.X }}`)
- `search.rows.selector` (CSS selectors, including template-expanded ones)
- Field extraction: `title`, `download`, `size`, `infohash`, `category`
- Basic filters: `replace`, `regexp`, `append`, `prepend`
- Basic filters: `replace`, `regexp`, `append`, `prepend`, `trim`, `tolower`, `re_replace`
- `settings:` block defaults populated automatically as `{{ .Config.X }}` values

**What doesn't work yet**:
- Login flows (cookie, form, POST)
- Multi-page pagination
- Custom headers beyond what httpx sends by default
- JavaScript-heavy sites that need Playwright/Puppeteer

## Catalog (public sites, one click)

For public, no-account torrent sites, Trove ships with a curated catalog of pre-configured definitions including 1337x, TorrentGalaxy, LimeTorrents, Nyaa, EZTV, and others. Five slugs map to substitute definitions (ExtraTorrent, KickAssTorrents, TorrentDownload, TorrentProject2, Tokyo Toshokan) because the original sites have no upstream Cardigann YAML — each entry honestly declares its actual source in the description.

The Pirate Bay and YTS are not in the current catalog because both return JSON responses that require a native driver rather than the HTML-scraping Cardigann parser. They are candidates for a follow-up JSON-indexer batch.

**How to install:**

1. On `/indexers`, click **Browse catalog** (next to **Add indexer**).
2. Pick a mirror from the dropdown on the site's tile — catalog entries list multiple known mirrors for sites that have them.
3. Click **Add**. The tile flips to **Installed** and the site appears on `/indexers` as a normal Cardigann indexer.

The onboarding wizard also surfaces these sites as an optional step — pick any you want in one go.

**Behind the scenes**: a catalog-installed entry is an ordinary `type=cardigann` indexer with a vendored YAML definition. The Test, Edit, and Delete buttons work the same way they do for hand-added indexers. If you ever need to override the URL or rename the entry, use **Edit** — nothing is special about catalog rows.

**Updating definitions**: the shipped YAMLs track `Prowlarr/Indexers`. When a site changes its HTML, searches will start returning 0 results. Upstream usually has a fix within days. A maintainer can run `scripts/update-catalog.py diff` to see what's changed upstream, then `sync` to pull — this is manual today, typically done alongside a release.

## Priority and ordering

Each indexer has a **priority** field (default 50). Lower numbers run first in the fan-out, but since Trove queries all enabled indexers in parallel, priority mainly matters for tie-breaking when the same release appears from multiple sources — the higher-priority indexer's copy is kept.
Expand Down
Loading