diff --git a/scripts/test-season-filtering.ts b/scripts/test-season-filtering.ts new file mode 100755 index 0000000..43a644c --- /dev/null +++ b/scripts/test-season-filtering.ts @@ -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(); diff --git a/src/formatters/content.ts b/src/formatters/content.ts index 2339259..1b42c74 100644 --- a/src/formatters/content.ts +++ b/src/formatters/content.ts @@ -83,6 +83,8 @@ function formatTorrent(t: TorrentInfo, compact?: boolean): string { export interface FormatOptions { compact?: boolean; + season?: number; + episode?: number; } function formatResult( @@ -102,12 +104,39 @@ function formatResult( } if (r.torrents.length > 0) { - const top = r.torrents - .sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0)) - .slice(0, 5); - lines.push(` Torrents (${r.torrents.length} total, top ${top.length}):`); - for (const t of top) { - lines.push(formatTorrent(t, opts?.compact)); + // 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); + 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"); diff --git a/src/index.ts b/src/index.ts index 7353a14..199899b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/prompts.ts b/src/prompts.ts index f6321fb..6cd4694 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -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`, }, }, ], diff --git a/src/resources/presentation-guide.ts b/src/resources/presentation-guide.ts new file mode 100644 index 0000000..f109909 --- /dev/null +++ b/src/resources/presentation-guide.ts @@ -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, + }, + ], + }; + }, + ); +} diff --git a/src/tools/search-content.ts b/src/tools/search-content.ts index d1772f2..d3d6a28 100644 --- a/src/tools/search-content.ts +++ b/src/tools/search-content.ts @@ -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, + }), }, ], }; diff --git a/tests/formatters/content.test.ts b/tests/formatters/content.test.ts index 4206386..cf8ca08 100644 --- a/tests/formatters/content.test.ts +++ b/tests/formatters/content.test.ts @@ -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", () => { diff --git a/tests/prompts.test.ts b/tests/prompts.test.ts index 0d74126..101b5f4 100644 --- a/tests/prompts.test.ts +++ b/tests/prompts.test.ts @@ -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); diff --git a/tests/resources/presentation-guide.test.ts b/tests/resources/presentation-guide.test.ts new file mode 100644 index 0000000..060ef55 --- /dev/null +++ b/tests/resources/presentation-guide.test.ts @@ -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"); + }); +});