feat(mcp): add season filtering and presentation guide for better UX

This commit is contained in:
Deivid Soto 2026-02-12 18:44:22 +01:00
parent b6f0af707c
commit d48b91f554
9 changed files with 941 additions and 18 deletions

248
scripts/test-season-filtering.ts Executable file
View file

@ -0,0 +1,248 @@
#!/usr/bin/env tsx
/**
* Script de prueba para verificar el filtrado por temporada/episodio
*
* Uso:
* npx tsx scripts/test-season-filtering.ts
*/
import { formatSearchResults } from "../src/formatters/content.js";
import type { SearchResponse, TorrentInfo } from "../src/types.js";
// Colores para la consola
const colors = {
reset: "\x1b[0m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
};
function log(color: keyof typeof colors, message: string) {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function header(message: string) {
console.log("\n" + "=".repeat(80));
log("cyan", message);
console.log("=".repeat(80));
}
function createTorrent(
season: number | null,
episode: number | null,
quality: string,
score: number,
seeders: number,
): TorrentInfo {
return {
infoHash: "a".repeat(40),
rawTitle: null,
quality,
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "1073741824",
seeders,
leechers: 0,
magnetUrl: `magnet:?xt=urn:btih:${"a".repeat(40)}`,
torrentUrl: null,
source: "test",
qualityScore: score,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season,
episode,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
};
}
function createResponse(torrents: TorrentInfo[]): SearchResponse {
return {
total: 1,
page: 1,
pageSize: 10,
results: [
{
id: 1,
imdbId: "tt1234567",
tmdbId: "12345",
contentType: "show",
title: "Test Show",
titleOriginal: null,
year: 2024,
overview:
"Una serie de prueba para verificar el filtrado por temporada",
posterUrl: null,
backdropUrl: null,
genres: ["Drama", "Action"],
ratingImdb: "8.5",
ratingTmdb: "8.2",
contentUrl: "https://torrentclaw.com/shows/test-show-1",
hasTorrents: true,
torrents,
},
],
};
}
// Test 1: Caso original del bug - Temporada 4 con mejores torrents en temporadas anteriores
header("TEST 1: Búsqueda de temporada específica (caso del bug original)");
log(
"yellow",
"Escenario: Serie con temporadas 1-4, donde T1-T3 tienen mejor calidad que T4",
);
log("yellow", "Búsqueda: season=4");
const test1Torrents = [
createTorrent(1, null, "1080p", 90, 100), // Mejor score, pero T1
createTorrent(2, null, "1080p", 85, 80), // Segundo mejor, pero T2
createTorrent(3, null, "1080p", 80, 60), // Tercero, pero T3
createTorrent(4, null, "720p", 70, 40), // T4 - score bajo
createTorrent(4, null, "1080p", 75, 30), // T4 - score medio
];
const test1Response = createResponse(test1Torrents);
console.log("\n📌 SIN filtro (comportamiento anterior):");
console.log(formatSearchResults(test1Response));
console.log("\n✅ CON filtro season=4 (comportamiento corregido):");
console.log(formatSearchResults(test1Response, { season: 4 }));
log("green", "✓ Debe mostrar SOLO los 2 torrents de temporada 4");
log("green", '✓ Debe indicar "2 matching, 5 total"');
// Test 2: Filtrado por episodio específico
header("TEST 2: Búsqueda de episodio específico");
log("yellow", "Escenario: Múltiples episodios de la misma temporada");
log("yellow", "Búsqueda: season=2, episode=5");
const test2Torrents = [
createTorrent(2, 3, "1080p", 90, 100),
createTorrent(2, 5, "1080p", 85, 80), // Episodio buscado
createTorrent(2, 5, "720p", 70, 60), // Episodio buscado
createTorrent(2, 7, "1080p", 88, 90),
];
const test2Response = createResponse(test2Torrents);
console.log("\n✅ CON filtro season=2, episode=5:");
console.log(formatSearchResults(test2Response, { season: 2, episode: 5 }));
log("green", "✓ Debe mostrar SOLO los 2 torrents de S02E05");
log("green", '✓ Debe indicar "2 matching, 4 total"');
// Test 3: Temporada no disponible
header("TEST 3: Temporada no disponible");
log("yellow", "Escenario: Buscar temporada que no existe");
log("yellow", "Búsqueda: season=10");
const test3Torrents = [
createTorrent(1, null, "1080p", 90, 100),
createTorrent(2, null, "1080p", 85, 80),
createTorrent(3, null, "720p", 75, 60),
];
const test3Response = createResponse(test3Torrents);
console.log("\n✅ CON filtro season=10 (no existe):");
console.log(formatSearchResults(test3Response, { season: 10 }));
log("green", '✓ Debe mostrar mensaje "No torrents available for season 10"');
log("green", '✓ Debe indicar "3 torrents available for other seasons"');
// Test 4: Packs de temporada vs episodios individuales
header("TEST 4: Packs de temporada completa vs episodios individuales");
log("yellow", "Escenario: Mezcla de packs completos y episodios individuales");
log("yellow", "Búsqueda: season=1, episode=5");
const test4Torrents = [
createTorrent(1, null, "1080p", 95, 100), // Pack completo T1
createTorrent(1, 1, "1080p", 90, 80), // Episodio 1
createTorrent(1, 5, "1080p", 85, 70), // Episodio 5 (buscado)
createTorrent(1, 5, "720p", 75, 60), // Episodio 5 (buscado)
createTorrent(1, 10, "1080p", 88, 65), // Episodio 10
];
const test4Response = createResponse(test4Torrents);
console.log("\n📌 CON filtro season=1 (solo temporada):");
console.log(formatSearchResults(test4Response, { season: 1 }));
log("green", "✓ Debe mostrar todos los torrents de T1 (5 torrents)");
console.log("\n✅ CON filtro season=1, episode=5 (específico):");
console.log(formatSearchResults(test4Response, { season: 1, episode: 5 }));
log("green", "✓ Debe mostrar SOLO episodios S01E05 (2 torrents)");
log("green", "✓ NO debe mostrar el pack completo");
// Test 5: Más de 5 torrents de la misma temporada
header("TEST 5: Más de 5 torrents de la misma temporada");
log("yellow", "Escenario: 8 torrents disponibles de la temporada 3");
log("yellow", "Búsqueda: season=3");
const test5Torrents = [
createTorrent(3, null, "2160p", 100, 150),
createTorrent(3, null, "1080p", 95, 140),
createTorrent(3, null, "1080p", 90, 130),
createTorrent(3, null, "720p", 85, 120),
createTorrent(3, null, "1080p", 80, 110),
createTorrent(3, null, "720p", 75, 100),
createTorrent(3, null, "480p", 70, 90),
createTorrent(3, null, "720p", 65, 80),
];
const test5Response = createResponse(test5Torrents);
console.log("\n✅ CON filtro season=3:");
console.log(formatSearchResults(test5Response, { season: 3 }));
log("green", "✓ Debe mostrar máximo 5 torrents (los de mejor score)");
log("green", '✓ Debe indicar "8 matching, 8 total, top 5"');
log("green", "✓ Primer torrent debe ser 2160p (score: 100)");
// Test 6: Sin filtro (comportamiento original debe mantenerse)
header("TEST 6: Sin filtro de temporada (regresión)");
log("yellow", "Escenario: Búsqueda sin especificar temporada");
log("yellow", "Búsqueda: sin season ni episode");
const test6Torrents = [
createTorrent(1, null, "1080p", 70, 50),
createTorrent(2, null, "1080p", 85, 80),
createTorrent(3, null, "2160p", 95, 100),
createTorrent(4, null, "720p", 60, 40),
];
const test6Response = createResponse(test6Torrents);
console.log("\n✅ SIN filtro:");
console.log(formatSearchResults(test6Response));
log("green", "✓ Debe mostrar top 4 torrents ordenados por score");
log("green", "✓ Primero: T3 2160p (score: 95)");
log("green", "✓ Último: T4 720p (score: 60)");
log("green", '✓ Debe indicar "4 total, top 4"');
// Resumen
header("RESUMEN DE PRUEBAS");
log("cyan", "Todos los tests manuales ejecutados.");
log(
"cyan",
"Verifica que los resultados coincidan con las expectativas marcadas con ✓",
);
console.log("\nPara ejecutar tests automatizados:");
log("blue", " npm test -- tests/formatters/content.test.ts");
console.log("\nPara ver cobertura completa:");
log("blue", " npm test");
console.log();

View file

@ -83,6 +83,8 @@ function formatTorrent(t: TorrentInfo, compact?: boolean): string {
export interface FormatOptions {
compact?: boolean;
season?: number;
episode?: number;
}
function formatResult(
@ -102,13 +104,40 @@ function formatResult(
}
if (r.torrents.length > 0) {
const top = r.torrents
// Filter torrents by season/episode if specified
let filteredTorrents = r.torrents;
if (opts?.season !== undefined) {
filteredTorrents = filteredTorrents.filter(
(t) => t.season === opts.season,
);
if (opts?.episode !== undefined) {
filteredTorrents = filteredTorrents.filter(
(t) => t.episode === opts.episode,
);
}
}
if (filteredTorrents.length > 0) {
const top = filteredTorrents
.sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0))
.slice(0, 5);
lines.push(` Torrents (${r.torrents.length} total, top ${top.length}):`);
const totalMsg =
filteredTorrents.length !== r.torrents.length
? `${filteredTorrents.length} matching, ${r.torrents.length} total`
: `${r.torrents.length} total`;
lines.push(` Torrents (${totalMsg}, top ${top.length}):`);
for (const t of top) {
lines.push(formatTorrent(t, opts?.compact));
}
} else {
const seasonEpStr =
opts?.episode !== undefined
? `S${String(opts.season).padStart(2, "0")}E${String(opts.episode).padStart(2, "0")}`
: `season ${opts?.season}`;
lines.push(
` No torrents available for ${seasonEpStr} (${r.torrents.length} torrents available for other seasons)`,
);
}
} else {
lines.push(" No torrents available");
}

View file

@ -13,6 +13,7 @@ import { registerAutocomplete } from "./tools/autocomplete.js";
import { registerTrackInteraction } from "./tools/track-interaction.js";
import { registerScanRequest } from "./tools/scan-request.js";
import { registerStatsResource } from "./resources/stats.js";
import { registerPresentationGuideResource } from "./resources/presentation-guide.js";
import { registerPrompts } from "./prompts.js";
const client = new TorrentClawClient();
@ -37,6 +38,7 @@ registerScanRequest(server, client);
// Register resources
registerStatsResource(server, client);
registerPresentationGuideResource(server);
// Register prompts
registerPrompts(server);

View file

@ -2,6 +2,57 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerPrompts(server: McpServer): void {
server.prompt(
"presentation_guide",
"Guide for presenting torrent search results in a user-friendly format",
{},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `When presenting torrent search results to users, follow these best practices:
1. **Magnet Links**: Always make magnet links clickable using markdown format:
- Format: [📥 Download](magnet:?xt=urn:btih:HASH...)
- Or: [🧲 Magnet Link](magnet:?xt=urn:btih:HASH...)
- Never show raw magnet URIs without making them clickable
2. **Content URL**: Include the TorrentClaw content URL for browsing all seasons/episodes:
- Format: [🔗 View all seasons on TorrentClaw](https://torrentclaw.com/shows/...)
- This allows users to explore other seasons/episodes
3. **Presentation Format**: Use clear, readable formatting:
- Group by episode/season for TV shows
- Show quality, size, and seeder count prominently
- Highlight torrents with active seeders
- Warn if torrents have 0 seeders
4. **Example Format for TV Shows**:
**Entrevías - Temporada 4**
**Episodio 1** (S04E01)
- 720p HDTV 879 MB 6 seeders [📥 Download](magnet:?xt=...)
**Episodio 2** (S04E02)
- 1080p WEB-DL 2.5 GB 0 seeders [📥 Download](magnet:?xt=...)
- 720p HDTV 976 MB 1 seeder [📥 Download](magnet:?xt=...)
[🔗 View all seasons on TorrentClaw](URL)
5. **Helpful Information**:
- Recommend torrents with more seeders
- Suggest alternatives if requested season/episode has no seeders
- Offer to search for different quality if user wants
Apply these practices to make results actionable and user-friendly.`,
},
},
],
}),
);
server.prompt(
"search_movie",
"Search for a movie by title and get torrent download options",
@ -12,7 +63,7 @@ export function registerPrompts(server: McpServer): void {
role: "user",
content: {
type: "text",
text: `Search for the movie "${title}" using search_content with type="movie". Present the results showing: title, year, ratings, and the top torrents sorted by quality score with their magnet links. If results are found, also call get_watch_providers with the content_id to check streaming availability.`,
text: `Search for the movie "${title}" using search_content with type="movie". Present the results with clickable magnet links using markdown format [📥 Download](magnet:...), include the content URL for more details, and show quality/size/seeders clearly. If results are found, also call get_watch_providers with the content_id to check streaming availability.`,
},
},
],
@ -22,14 +73,23 @@ export function registerPrompts(server: McpServer): void {
server.prompt(
"search_show",
"Search for a TV show by title and get torrent download options",
{ title: z.string().describe("TV show title to search for") },
({ title }) => ({
{
title: z.string().describe("TV show title to search for"),
season: z.number().optional().describe("Specific season number"),
},
({ title, season }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Search for the TV show "${title}" using search_content with type="show". Present the results showing: title, year, ratings, and the top torrents sorted by quality score with their magnet links.`,
text: `Search for the TV show "${title}" using search_content with type="show"${season ? ` and season=${season}` : ""}. Present results grouped by episode with:
- Episode identifier (e.g., S04E01)
- Quality, size, and seeder count
- Clickable magnet links using markdown: [📥 Download](magnet:...)
- Content URL for browsing all seasons: [🔗 View all seasons](URL)
- Recommendations for torrents with most seeders
- Warnings if torrents have 0 seeders`,
},
},
],

View file

@ -0,0 +1,137 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerPresentationGuideResource(server: McpServer): void {
server.resource(
"presentation-guide",
"torrentclaw://presentation-guide",
{
description:
"Best practices for presenting torrent search results to users in a user-friendly, actionable format",
mimeType: "text/markdown",
},
async (uri) => {
const guide = `# TorrentClaw Results Presentation Guide
## Critical Requirements
When presenting torrent search results to users, you MUST follow these practices to make results actionable and user-friendly:
### 1. Clickable Magnet Links
**ALWAYS** make magnet links clickable using markdown format:
**CORRECT**:
- [📥 Download](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...)
- [🧲 Magnet](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...)
**INCORRECT**:
- magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4 (not clickable)
- Showing only the info hash without full magnet URI
### 2. Content URL for Browsing
**ALWAYS** include the TorrentClaw content URL so users can explore all seasons/episodes:
**CORRECT**:
[🔗 View all seasons and episodes on TorrentClaw](https://torrentclaw.com/shows/entrevias-2022-91260)
This allows users to:
- Browse all available seasons
- See all torrents for each episode
- Explore different quality options
### 3. User-Friendly Presentation Format
**For TV Shows** (especially when searching by season):
\`\`\`markdown
### Entrevías - Temporada 4
**Episodio 1** (S04E01)
- 720p HDTV 879 MB 6 seeders [📥 Download](magnet:?xt=urn:btih:...)
**Episodio 2** (S04E02)
- 1080p WEB-DL 2.5 GB 0 seeders No active seeders [📥 Download](magnet:?xt=urn:btih:...)
- 720p HDTV 976 MB 1 seeder [📥 Download](magnet:?xt=urn:btih:...)
**Episodio 3** (S04E03)
- 720p HDTV 795 MB 2 seeders [📥 Download](magnet:?xt=urn:btih:...)
[🔗 View all seasons on TorrentClaw](https://torrentclaw.com/shows/...)
\`\`\`
**For Movies**:
\`\`\`markdown
### Inception (2010)
IMDb: 8.8 | TMDB: 8.4
**Available Torrents:**
1. **2160p BluRay** 15.2 GB 147 seeders [📥 Download](magnet:?xt=...)
2. **1080p BluRay** 2.0 GB 847 seeders Recommended [📥 Download](magnet:?xt=...)
3. **720p WEB-DL** 1.2 GB 234 seeders [📥 Download](magnet:?xt=...)
[🔗 View on TorrentClaw](https://torrentclaw.com/movies/...)
\`\`\`
### 4. Helpful User Guidance
Provide context and recommendations:
- Recommend torrents with most seeders
- Warn when torrents have 0 seeders: "⚠️ No active seeders"
- Mark best option: "⭐ Recommended" (based on seeders + quality)
- Suggest alternatives if requested season has no seeders
- Offer to search different quality/season
### 5. What NOT to Do
**Never** present results in plain text tables without clickable links
**Never** show truncated magnet links
**Never** omit the content URL
**Never** show info hashes without the full magnet URI
**Never** present results without indicating seeder count
### 6. Example of Good vs Bad Presentation
** BAD** (what user reported as not practical):
\`\`\`
Episodio Calidad Tamaño Seeders Magnet
S04E01 720p HDTV 879 MB 6 seeders magnet:?xt=urn:btih:41159dc...
\`\`\`
** GOOD**:
\`\`\`markdown
**S04E01**
720p HDTV 879 MB 6 seeders [📥 Download](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...)
[🔗 View all episodes on TorrentClaw](https://torrentclaw.com/shows/entrevias-2022-91260)
\`\`\`
## Summary
The key is to make results **actionable**: users should be able to:
1. Click magnet links to start downloading immediately
2. Click content URL to explore more options
3. Quickly identify which torrents are best (seeders)
4. Understand warnings (no seeders)
**Remember**: You're not just displaying data, you're helping users take action.
`;
return {
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: guide,
},
],
};
},
);
}

View file

@ -10,7 +10,7 @@ export function registerSearchContent(
): void {
server.tool(
"search_content",
"Search for movies and TV shows by title, genre, year, rating, or quality. Returns matching content with metadata (title, year, genres, IMDb/TMDB ratings) and torrent download options (magnet links, quality, seeders, file size). This is the primary tool — use it first when a user asks to find, download, or learn about a movie or TV show. Results include a content_id needed by get_watch_providers and get_credits. For TV shows, you can filter by season/episode. Season/episode can also be auto-detected from the query (e.g. 'Bluey s01e05').",
"Search for movies and TV shows by title, genre, year, rating, or quality. Returns matching content with metadata (title, year, genres, IMDb/TMDB ratings) and torrent download options (magnet links, quality, seeders, file size). This is the primary tool — use it first when a user asks to find, download, or learn about a movie or TV show. Results include a content_id needed by get_watch_providers and get_credits. For TV shows, you can filter by season/episode. Season/episode can also be auto-detected from the query (e.g. 'Bluey s01e05'). IMPORTANT: When presenting results to users, make magnet links clickable using markdown format [Download](magnet:?xt=...), include the contentUrl for browsing all seasons/episodes, and present the information in a user-friendly format rather than raw tables.",
{
query: z
.string()
@ -176,7 +176,11 @@ export function registerSearchContent(
content: [
{
type: "text",
text: formatSearchResults(data, { compact: params.compact }),
text: formatSearchResults(data, {
compact: params.compact,
season: params.season,
episode: params.episode,
}),
},
],
};

View file

@ -745,9 +745,27 @@ describe("formatSearchResults", () => {
season: null,
episode: null,
audioTracks: [
{ lang: "en", codec: "aac", channels: "5.1", title: "English", default: true },
{ lang: "es", codec: "aac", channels: "5.1", title: "Spanish", default: false },
{ lang: "en", codec: "ac3", channels: "2.0", title: "Commentary", default: false },
{
lang: "en",
codec: "aac",
channels: "5.1",
title: "English",
default: true,
},
{
lang: "es",
codec: "aac",
channels: "5.1",
title: "Spanish",
default: false,
},
{
lang: "en",
codec: "ac3",
channels: "2.0",
title: "Commentary",
default: false,
},
],
subtitleTracks: null,
videoInfo: null,
@ -809,7 +827,13 @@ describe("formatSearchResults", () => {
season: null,
episode: null,
audioTracks: [
{ lang: null, codec: null, channels: null, title: null, default: null },
{
lang: null,
codec: null,
channels: null,
title: null,
default: null,
},
],
subtitleTracks: null,
videoInfo: null,
@ -1021,6 +1045,346 @@ describe("formatSearchResults", () => {
expect(text).toContain("Stream: Netflix, Disney+");
expect(text).toContain("Free: Tubi");
});
it("filters torrents by season when specified", () => {
const response: SearchResponse = {
total: 1,
page: 1,
pageSize: 10,
results: [
{
id: 1,
imdbId: null,
tmdbId: null,
contentType: "show",
title: "Test Show",
titleOriginal: null,
year: 2024,
overview: null,
posterUrl: null,
backdropUrl: null,
genres: null,
ratingImdb: null,
ratingTmdb: null,
contentUrl: null,
hasTorrents: true,
torrents: [
{
infoHash: "a".repeat(40),
rawTitle: null,
quality: "1080p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "1073741824",
seeders: 100,
leechers: 5,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 90,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 1,
episode: null,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
{
infoHash: "b".repeat(40),
rawTitle: null,
quality: "720p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "536870912",
seeders: 50,
leechers: 2,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 70,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 4,
episode: null,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
{
infoHash: "c".repeat(40),
rawTitle: null,
quality: "1080p",
codec: "x265",
sourceType: "WEB-DL",
sizeBytes: "805306368",
seeders: 75,
leechers: 3,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 85,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 4,
episode: null,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
],
},
],
};
// Filter by season 4
const text = formatSearchResults(response, { season: 4 });
expect(text).toContain("2 matching, 3 total");
// Should show season 4 torrents (quality scores 70 and 85)
expect(text).toContain("Score: 85");
expect(text).toContain("Score: 70");
// Should NOT show season 1 torrent (quality score 90)
expect(text).not.toContain("Score: 90");
});
it("filters torrents by season and episode when both specified", () => {
const response: SearchResponse = {
total: 1,
page: 1,
pageSize: 10,
results: [
{
id: 1,
imdbId: null,
tmdbId: null,
contentType: "show",
title: "Test Show",
titleOriginal: null,
year: 2024,
overview: null,
posterUrl: null,
backdropUrl: null,
genres: null,
ratingImdb: null,
ratingTmdb: null,
contentUrl: null,
hasTorrents: true,
torrents: [
{
infoHash: "a".repeat(40),
rawTitle: null,
quality: "1080p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "1073741824",
seeders: 100,
leechers: 5,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 90,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 2,
episode: 5,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
{
infoHash: "b".repeat(40),
rawTitle: null,
quality: "720p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "536870912",
seeders: 50,
leechers: 2,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 70,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 2,
episode: 3,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
],
},
],
};
// Filter by season 2, episode 5
const text = formatSearchResults(response, { season: 2, episode: 5 });
expect(text).toContain("1 matching, 2 total");
// Should show only S02E05 torrent
expect(text).toContain("Score: 90");
expect(text).toContain("S02E05");
// Should NOT show S02E03
expect(text).not.toContain("Score: 70");
});
it("shows message when no torrents match the season filter", () => {
const response: SearchResponse = {
total: 1,
page: 1,
pageSize: 10,
results: [
{
id: 1,
imdbId: null,
tmdbId: null,
contentType: "show",
title: "Test Show",
titleOriginal: null,
year: 2024,
overview: null,
posterUrl: null,
backdropUrl: null,
genres: null,
ratingImdb: null,
ratingTmdb: null,
contentUrl: null,
hasTorrents: true,
torrents: [
{
infoHash: "a".repeat(40),
rawTitle: null,
quality: "1080p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "1073741824",
seeders: 100,
leechers: 5,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 90,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 1,
episode: null,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
],
},
],
};
// Filter by season 4 (not available)
const text = formatSearchResults(response, { season: 4 });
expect(text).toContain("No torrents available for season 4");
expect(text).toContain("1 torrents available for other seasons");
});
it("shows message when no torrents match the season+episode filter", () => {
const response: SearchResponse = {
total: 1,
page: 1,
pageSize: 10,
results: [
{
id: 1,
imdbId: null,
tmdbId: null,
contentType: "show",
title: "Test Show",
titleOriginal: null,
year: 2024,
overview: null,
posterUrl: null,
backdropUrl: null,
genres: null,
ratingImdb: null,
ratingTmdb: null,
contentUrl: null,
hasTorrents: true,
torrents: [
{
infoHash: "a".repeat(40),
rawTitle: null,
quality: "1080p",
codec: "x264",
sourceType: "WEB-DL",
sizeBytes: "1073741824",
seeders: 100,
leechers: 5,
magnetUrl: null,
torrentUrl: null,
source: "test",
qualityScore: 90,
uploadedAt: null,
languages: [],
audioCodec: null,
hdrType: null,
releaseGroup: null,
isProper: null,
isRepack: null,
isRemastered: null,
season: 2,
episode: 5,
audioTracks: null,
subtitleTracks: null,
videoInfo: null,
scanStatus: null,
},
],
},
],
};
// Filter by S04E01 (not available)
const text = formatSearchResults(response, { season: 4, episode: 1 });
expect(text).toContain("No torrents available for S04E01");
expect(text).toContain("1 torrents available for other seasons");
});
});
describe("formatPopularResults", () => {

View file

@ -26,10 +26,11 @@ describe("registerPrompts", () => {
return { server, prompts };
}
it("registers 4 prompts", () => {
it("registers 5 prompts", () => {
const { server, prompts } = createMockServer();
registerPrompts(server);
expect(prompts.size).toBe(4);
expect(prompts.size).toBe(5);
expect(prompts.has("presentation_guide")).toBe(true);
expect(prompts.has("search_movie")).toBe(true);
expect(prompts.has("search_show")).toBe(true);
expect(prompts.has("whats_new")).toBe(true);

View file

@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { createMockServer } from "../helpers.js";
import { registerPresentationGuideResource } from "../../src/resources/presentation-guide.js";
describe("presentation-guide resource", () => {
it("returns markdown guide with best practices", async () => {
const { server, getResourceHandler } = createMockServer();
registerPresentationGuideResource(server);
const handler = getResourceHandler("torrentclaw://presentation-guide");
const result = await handler({ href: "torrentclaw://presentation-guide" });
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe("text/markdown");
const text = result.contents[0].text;
// Check for key sections
expect(text).toContain("# TorrentClaw Results Presentation Guide");
expect(text).toContain("## Critical Requirements");
expect(text).toContain("### 1. Clickable Magnet Links");
expect(text).toContain("### 2. Content URL for Browsing");
expect(text).toContain("### 3. User-Friendly Presentation Format");
// Check for markdown examples
expect(text).toContain("[📥 Download](magnet:");
expect(text).toContain("[🔗 View");
// Check for good vs bad examples
expect(text).toContain("❌ BAD");
expect(text).toContain("✅ GOOD");
// Check for warnings about seeders
expect(text).toContain("⚠️ No active seeders");
expect(text).toContain("⭐ Recommended");
});
it("provides guidance for TV shows", async () => {
const { server, getResourceHandler } = createMockServer();
registerPresentationGuideResource(server);
const handler = getResourceHandler("torrentclaw://presentation-guide");
const result = await handler({ href: "torrentclaw://presentation-guide" });
const text = result.contents[0].text;
expect(text).toContain("**For TV Shows**");
expect(text).toContain("S04E01");
expect(text).toContain("Entrevías");
});
it("provides guidance for movies", async () => {
const { server, getResourceHandler } = createMockServer();
registerPresentationGuideResource(server);
const handler = getResourceHandler("torrentclaw://presentation-guide");
const result = await handler({ href: "torrentclaw://presentation-guide" });
const text = result.contents[0].text;
expect(text).toContain("**For Movies**");
expect(text).toContain("Inception");
expect(text).toContain("BluRay");
});
it("warns against bad practices", async () => {
const { server, getResourceHandler } = createMockServer();
registerPresentationGuideResource(server);
const handler = getResourceHandler("torrentclaw://presentation-guide");
const result = await handler({ href: "torrentclaw://presentation-guide" });
const text = result.contents[0].text;
// Check for warnings
expect(text).toContain("### 5. What NOT to Do");
expect(text).toContain("without clickable links");
expect(text).toContain("truncated magnet links");
expect(text).toContain("omit the content URL");
});
});