Use this skill when the user asks to add support for a new torrent tracker.
Install
mkdir -p .claude/skills/add-tracker && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13332" && unzip -o skill.zip -d .claude/skills/add-tracker && rm skill.zipInstalls to .claude/skills/add-tracker
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Use this skill when the user asks to add support for a new torrent tracker.About this skill
Skill: Adding a New Tracker
When to Use
Use this skill when the user asks to add support for a new torrent tracker.
Overview
Trackers live in src/lib/server/trackers/. Each tracker is a single TypeScript file that extends the abstract Tracker base class from src/lib/server/tracker.ts. After creating the file, it must be registered in src/lib/server/trackers/index.ts.
Most trackers in this project are Unit3D-based. Use src/lib/server/trackers/lst.ts as the primary reference for Unit3D trackers, and src/lib/server/trackers/aither.ts as a secondary reference.
Step-by-Step
1. Create the tracker file
Create src/lib/server/trackers/<trackername>.ts (lowercase). The file must export:
default— the tracker class (extendsTracker)settings— aSettingsField[]array for the settings UIfields— aTrackerField[]array (withas const satisfies TrackerField[]) defining form fields
2. Register in index.ts
Add the tracker to src/lib/server/trackers/index.ts:
import NewTracker, { settings as newtrackerSettings, fields as newtrackerFields } from './newtracker';
// Then add to the trackers record:
'NewTracker': { class: NewTracker, settings: newtrackerSettings, fields: newtrackerFields },
3. Run type checking
Run bun run check to verify the tracker compiles correctly.
File Structure of a Tracker
Every tracker file follows this structure in order:
Imports
import type { FieldsToType, KeyValueData, SettingsField, TrackerField, TrackerSearchResults, TrackerSettings, TrackerAfterUploadAction, Metadata, TrackerLayout } from '$lib/types';
import * as v from 'valibot';
import type Release from '../release';
import Tracker from '../tracker';
import { unit3dDistributors, unit3dRegions } from './unit3d-distributors';
import { log } from '../util/log';
import errorString from '../util/error-string';
import { TTLCache } from '@isaacs/ttlcache';
import pMemoize from 'p-memoize';
Only import unit3d-distributors if the tracker is Unit3D-based.
URL Constants
Define API endpoint URLs as module-level constants:
const UPLOAD_URL = 'https://example.com/api/torrents/upload';
const SEARCH_URL = 'https://example.com/api/torrents/filter';
const BANNED_GROUPS_URL = 'https://example.com/api/bannedReleaseGroups';
Valibot Schemas
Define response validation schemas near the top, before the data arrays:
const SearchResultsSchema = v.object({
data: v.array(v.object({
id: v.string(),
attributes: v.object({
name: v.string(),
details_link: v.pipe(v.string(), v.url()),
}),
})),
links: v.object({
next: v.nullable(v.pipe(v.string(), v.url())),
}),
});
KeyValueData Arrays
Define categories, types, resolutions, and any tracker-specific option arrays as KeyValueData (which is [key: string, value: string][]). The first element is the API ID, the second is the display label:
const categories: KeyValueData = [
['1', 'Movies'],
['2', 'TV'],
];
const types: KeyValueData = [
['1', 'Full Disc'],
['2', 'Remux'],
['3', 'Encode'],
['4', 'WEB-DL'],
['5', 'WEBRip'],
['6', 'HDTV'],
['7', 'Other'],
];
const resolutions: KeyValueData = [
['1', '4320p'],
['2', '2160p'],
['3', '1080p'],
['4', '1080i'],
['5', '720p'],
['6', '576p'],
['7', '576i'],
['8', '480p'],
['9', '480i'],
['10', 'Other'],
];
These values are tracker-specific — check the tracker's API or upload page for the correct IDs and labels.
Settings Export
The settings export defines what appears in the app's Settings page for this tracker. Common fields:
export const settings: SettingsField[] = [
{
id: 'announce',
label: 'Announce URL',
type: 'password',
description: 'You can find your announce URL on the <a href="https://example.com/upload">upload page</a>.',
}, {
id: 'apiKey',
label: 'API key',
type: 'password',
description: 'Your API key can be found in your profile settings.',
}, {
id: 'defaultDescription',
label: 'Default description',
type: 'multiline',
default: '{% screenshots width:350 %}[url={{page}}][img=350]{{thumbnail}}[/img][/url]{% endscreenshots %}',
}
];
Fields Export
The fields export defines the upload form fields. It MUST use as const satisfies TrackerField[] for type inference. Each field has:
key— unique camelCase identifier, used inthis.dataand the API (e.g.categoryId,seasonNumber,personalRelease)label— display name in the UItype—'text','multiline','select', or'checkbox'default— default value (string for text/multiline/select, boolean for checkbox). For select fields, the default is matched by the display label (second element of the KeyValueData tuple), not the keyoptions— required for select fields, aKeyValueDataarraysize— optional, controls UI width
Standard fields most trackers have:
export const fields = [
{ key: 'name', label: 'Title', type: 'text', default: '' },
{ key: 'categoryId', label: 'Category', type: 'select', default: 'Movies', options: categories, size: 13 },
{ key: 'typeId', label: 'Type', type: 'select', default: 'Other', options: types, size: 13 },
{ key: 'resolutionId', label: 'Resolution', type: 'select', default: 'Other', options: resolutions, size: 13 },
{ key: 'seasonNumber', label: 'Season', type: 'text', default: '', size: 3 },
{ key: 'episodeNumber', label: 'Episode', type: 'text', default: '', size: 3 },
{ key: 'tmdb', label: 'TMDB ID', type: 'text', default: '', size: 10 },
{ key: 'imdb', label: 'IMDB ID', type: 'text', default: '', size: 10 },
{ key: 'tvdb', label: 'TVDB ID', type: 'text', default: '', size: 10 },
{ key: 'mal', label: 'MAL ID', type: 'text', default: '', size: 10 },
{ key: 'keywords', label: 'Keywords', type: 'text', default: '' },
{ key: 'description', label: 'Description', type: 'multiline', default: '{% screenshots width:350 %}[url={{page}}][img=350]{{thumbnail}}[/img][/url]{% endscreenshots %}' },
{ key: 'mediainfo', label: 'MediaInfo', type: 'multiline', default: '{{ mediaInfo.fullText }}' },
{ key: 'bdinfo', label: 'BDInfo', type: 'multiline', default: '' },
{ key: 'anonymous', label: 'Anonymous', type: 'checkbox', default: false },
{ key: 'free', label: 'Freeleech', type: 'select', default: 'No Freeleech', options: frees, size: 16 },
] as const satisfies TrackerField[];
Layout
The layout defines a 2D grid for UI rendering. Each row is an array of field keys (or null for empty cells). Repeating a key across columns makes it span multiple columns:
const layout = [
['name', 'name', 'name', 'name'],
['categoryId', 'typeId', 'resolutionId'],
['seasonNumber', 'episodeNumber'],
['tmdb', 'imdb', 'tvdb', 'mal'],
['keywords', 'keywords', 'keywords', 'keywords'],
['description', 'description', 'description', 'description'],
['mediainfo', 'mediainfo', 'mediainfo', 'mediainfo'],
['bdinfo', 'bdinfo', 'bdinfo', 'bdinfo'],
['anonymous', 'free'],
] as const satisfies TrackerLayout;
The Tracker Class
export default class NewTracker extends Tracker {
apiKey: string = '';
override name: string = 'NewTracker';
override data: FieldsToType<typeof fields>;
override readonly fields = fields;
override readonly layout = layout;
source: string = 'NewTracker';
constructor(settings: TrackerSettings) {
super(settings);
this.data = this.setDefaults(this.fields);
if (!settings.apiKey) throw Error('API key is missing for NewTracker');
this.apiKey = settings.apiKey;
if (settings.defaultDescription) this.data.description = settings.defaultDescription;
}
Required properties:
name— display namedata— typed asFieldsToType<typeof fields>, initialized viathis.setDefaults(this.fields)fields— must beoverride readonlyreferencing the exportedfieldslayout— must beoverride readonlyreferencing thelayoutconstantsource— string used as metadata in generated torrents
Required Methods
applyMetadata(metadata: Metadata)
Populates tracker fields from TMDB/MAL data. Standard implementation for Unit3D trackers (note: Unit3D expects the IMDb ID as a bare number — the tt prefix must be stripped):
applyMetadata(metadata: Metadata) {
this.data.tmdb = String(metadata.tmdbId);
this.data.imdb = metadata.imdbId ? metadata.imdbId.replace(/^tt/i, '') : '0';
this.data.tvdb = metadata.tvdbId ? String(metadata.tvdbId) : '0';
this.data.mal = metadata.malId ? String(metadata.malId) : '0';
this.data.keywords = metadata.keywords.join(', ');
}
applyRelease(release: Release)
Auto-populates fields from parsed release info. This method should:
- Set resolution via
this.setOption('resolutionId', release.resolution) - Determine and set the type (Full Disc, Remux, Encode, WEB-DL, WEBRip, HDTV)
- Set category (Movie vs TV)
- Set season/episode numbers for TV
- Set HDR/DV flags if applicable
- Build a title format string and call
release.format(titleFormat)
Use this.setOption(fieldKey, displayValue) to set select fields by their display label (not their key ID).
Title format tokens (used in release.format()):
{title},{title aka}— release title, optionally with AKA{year}— release year{season_episode}— e.g.S01E05{season_or_episode_title}— episode title if available{edition}— edition i
Content truncated.