Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
18 changed files with 82 additions and 1055 deletions
|
|
@ -1,68 +0,0 @@
|
||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: docker.io/library/node:22
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: npm test
|
|
||||||
|
|
||||||
- name: Publish to npm
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Set authToken explicitly — actions/setup-node was the only consumer
|
|
||||||
# of registry-url + NODE_AUTH_TOKEN, and we dropped it for Forgejo
|
|
||||||
# compat. The literal npmjs registry stays the same.
|
|
||||||
cat > ~/.npmrc <<EOF
|
|
||||||
registry=https://registry.npmjs.org/
|
|
||||||
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
|
||||||
EOF
|
|
||||||
npm publish
|
|
||||||
rm -f ~/.npmrc
|
|
||||||
|
|
||||||
- name: Create Forgejo release with auto-generated notes
|
|
||||||
env:
|
|
||||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
FORGEJO_API: http://forgejo:3000/api/v1
|
|
||||||
REPO: torrentclaw/torrentclaw-mcp
|
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
apt-get update && apt-get install -y --no-install-recommends jq curl
|
|
||||||
|
|
||||||
# Compose release body from commits since the previous tag.
|
|
||||||
prev=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
|
||||||
if [ -n "$prev" ]; then
|
|
||||||
notes=$(git log --pretty=format:'- %s' "${prev}..${TAG}")
|
|
||||||
else
|
|
||||||
notes=$(git log --pretty=format:'- %s' "${TAG}")
|
|
||||||
fi
|
|
||||||
body=$(jq -n --arg t "$TAG" --arg n "$notes" \
|
|
||||||
'{tag_name:$t, name:$t, body:$n, draft:false, prerelease:false}')
|
|
||||||
curl -sSf -X POST "$FORGEJO_API/repos/$REPO/releases" \
|
|
||||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$body" >/dev/null || \
|
|
||||||
echo "Release may already exist for $TAG"
|
|
||||||
|
|
@ -12,33 +12,33 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
lint-commits:
|
lint-commits:
|
||||||
name: Lint commits
|
name: Lint commits
|
||||||
runs-on: docker
|
runs-on: ubuntu-latest
|
||||||
container:
|
|
||||||
image: docker.io/library/node:22
|
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Validate conventional commits
|
- name: Validate conventional commits
|
||||||
run: |
|
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||||
npx commitlint \
|
|
||||||
--from ${{ github.event.pull_request.base.sha }} \
|
|
||||||
--to ${{ github.event.pull_request.head.sha }} \
|
|
||||||
--verbose
|
|
||||||
|
|
||||||
build-and-test:
|
build-and-test:
|
||||||
name: Build & test
|
name: Build & test
|
||||||
runs-on: docker
|
runs-on: ubuntu-latest
|
||||||
container:
|
|
||||||
image: docker.io/library/node:22
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
41
.github/workflows/release.yml
vendored
Normal file
41
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -2,24 +2,6 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
## [0.2.2](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.2.1...v0.2.2) (2026-02-16)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- add glama.json and smithery.yaml for directory listings ([06865a2](https://github.com/torrentclaw/torrentclaw-mcp/commit/06865a2abda420567af7fb3d5046b29ea7de6060))
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- update qs to 6.15.0 to resolve CVE denial of service vulnerability ([657910a](https://github.com/torrentclaw/torrentclaw-mcp/commit/657910ad357f14f651d1af1afec0c0cda64513f5))
|
|
||||||
|
|
||||||
## [0.2.1](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.2.0...v0.2.1) (2026-02-12)
|
|
||||||
|
|
||||||
## [0.2.0](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.1.0...v0.2.0) (2026-02-12)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- **mcp:** add season filtering and presentation guide for better UX ([7844b83](https://github.com/torrentclaw/torrentclaw-mcp/commit/7844b83eb3f49323e7b64ca4c4e09868bbce2dd9))
|
|
||||||
|
|
||||||
## 0.1.0 (2026-02-12)
|
## 0.1.0 (2026-02-12)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 Deivid Soto
|
Copyright (c) 2026 buryni
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
|
||||||
"maintainers": ["torrentclaw", "eividsoto"]
|
|
||||||
}
|
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@torrentclaw/mcp",
|
"name": "torrentclaw-mcp",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@torrentclaw/mcp",
|
"name": "torrentclaw-mcp",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
|
|
@ -5003,9 +5003,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "@torrentclaw/mcp",
|
"name": "torrentclaw-mcp",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"description": "MCP server for TorrentClaw — search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata",
|
"description": "MCP server for TorrentClaw — search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"torrentclaw-mcp": "build/index.js"
|
"torrentclaw-mcp": "./build/index.js"
|
||||||
},
|
},
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/torrentclaw/torrentclaw-mcp.git"
|
"url": "https://github.com/torrentclaw/torrentclaw-mcp"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/torrentclaw/torrentclaw-mcp#readme",
|
"homepage": "https://github.com/torrentclaw/torrentclaw-mcp#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
#!/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();
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
runtime: "typescript"
|
|
||||||
|
|
@ -83,8 +83,6 @@ function formatTorrent(t: TorrentInfo, compact?: boolean): string {
|
||||||
|
|
||||||
export interface FormatOptions {
|
export interface FormatOptions {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
season?: number;
|
|
||||||
episode?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatResult(
|
function formatResult(
|
||||||
|
|
@ -104,39 +102,12 @@ function formatResult(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.torrents.length > 0) {
|
if (r.torrents.length > 0) {
|
||||||
// Filter torrents by season/episode if specified
|
const top = r.torrents
|
||||||
let filteredTorrents = r.torrents;
|
.sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0))
|
||||||
if (opts?.season !== undefined) {
|
.slice(0, 5);
|
||||||
filteredTorrents = filteredTorrents.filter(
|
lines.push(` Torrents (${r.torrents.length} total, top ${top.length}):`);
|
||||||
(t) => t.season === opts.season,
|
for (const t of top) {
|
||||||
);
|
lines.push(formatTorrent(t, opts?.compact));
|
||||||
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 {
|
} else {
|
||||||
lines.push(" No torrents available");
|
lines.push(" No torrents available");
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { registerAutocomplete } from "./tools/autocomplete.js";
|
||||||
import { registerTrackInteraction } from "./tools/track-interaction.js";
|
import { registerTrackInteraction } from "./tools/track-interaction.js";
|
||||||
import { registerScanRequest } from "./tools/scan-request.js";
|
import { registerScanRequest } from "./tools/scan-request.js";
|
||||||
import { registerStatsResource } from "./resources/stats.js";
|
import { registerStatsResource } from "./resources/stats.js";
|
||||||
import { registerPresentationGuideResource } from "./resources/presentation-guide.js";
|
|
||||||
import { registerPrompts } from "./prompts.js";
|
import { registerPrompts } from "./prompts.js";
|
||||||
|
|
||||||
const client = new TorrentClawClient();
|
const client = new TorrentClawClient();
|
||||||
|
|
@ -38,7 +37,6 @@ registerScanRequest(server, client);
|
||||||
|
|
||||||
// Register resources
|
// Register resources
|
||||||
registerStatsResource(server, client);
|
registerStatsResource(server, client);
|
||||||
registerPresentationGuideResource(server);
|
|
||||||
|
|
||||||
// Register prompts
|
// Register prompts
|
||||||
registerPrompts(server);
|
registerPrompts(server);
|
||||||
|
|
|
||||||
|
|
@ -2,57 +2,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerPrompts(server: McpServer): void {
|
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(
|
server.prompt(
|
||||||
"search_movie",
|
"search_movie",
|
||||||
"Search for a movie by title and get torrent download options",
|
"Search for a movie by title and get torrent download options",
|
||||||
|
|
@ -63,7 +12,7 @@ Apply these practices to make results actionable and user-friendly.`,
|
||||||
role: "user",
|
role: "user",
|
||||||
content: {
|
content: {
|
||||||
type: "text",
|
type: "text",
|
||||||
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.`,
|
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.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -73,23 +22,14 @@ Apply these practices to make results actionable and user-friendly.`,
|
||||||
server.prompt(
|
server.prompt(
|
||||||
"search_show",
|
"search_show",
|
||||||
"Search for a TV show by title and get torrent download options",
|
"Search for a TV show by title and get torrent download options",
|
||||||
{
|
{ title: z.string().describe("TV show title to search for") },
|
||||||
title: z.string().describe("TV show title to search for"),
|
({ title }) => ({
|
||||||
season: z.number().optional().describe("Specific season number"),
|
|
||||||
},
|
|
||||||
({ title, season }) => ({
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: {
|
content: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Search for the TV show "${title}" using search_content with type="show"${season ? ` and season=${season}` : ""}. Present results grouped by episode with:
|
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.`,
|
||||||
- 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`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function registerSearchContent(
|
||||||
): void {
|
): void {
|
||||||
server.tool(
|
server.tool(
|
||||||
"search_content",
|
"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'). 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.",
|
"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').",
|
||||||
{
|
{
|
||||||
query: z
|
query: z
|
||||||
.string()
|
.string()
|
||||||
|
|
@ -176,11 +176,7 @@ export function registerSearchContent(
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: formatSearchResults(data, {
|
text: formatSearchResults(data, { compact: params.compact }),
|
||||||
compact: params.compact,
|
|
||||||
season: params.season,
|
|
||||||
episode: params.episode,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -745,27 +745,9 @@ describe("formatSearchResults", () => {
|
||||||
season: null,
|
season: null,
|
||||||
episode: null,
|
episode: null,
|
||||||
audioTracks: [
|
audioTracks: [
|
||||||
{
|
{ lang: "en", codec: "aac", channels: "5.1", title: "English", default: true },
|
||||||
lang: "en",
|
{ lang: "es", codec: "aac", channels: "5.1", title: "Spanish", default: false },
|
||||||
codec: "aac",
|
{ lang: "en", codec: "ac3", channels: "2.0", title: "Commentary", default: false },
|
||||||
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,
|
subtitleTracks: null,
|
||||||
videoInfo: null,
|
videoInfo: null,
|
||||||
|
|
@ -827,13 +809,7 @@ describe("formatSearchResults", () => {
|
||||||
season: null,
|
season: null,
|
||||||
episode: null,
|
episode: null,
|
||||||
audioTracks: [
|
audioTracks: [
|
||||||
{
|
{ lang: null, codec: null, channels: null, title: null, default: null },
|
||||||
lang: null,
|
|
||||||
codec: null,
|
|
||||||
channels: null,
|
|
||||||
title: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
subtitleTracks: null,
|
subtitleTracks: null,
|
||||||
videoInfo: null,
|
videoInfo: null,
|
||||||
|
|
@ -1045,346 +1021,6 @@ describe("formatSearchResults", () => {
|
||||||
expect(text).toContain("Stream: Netflix, Disney+");
|
expect(text).toContain("Stream: Netflix, Disney+");
|
||||||
expect(text).toContain("Free: Tubi");
|
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", () => {
|
describe("formatPopularResults", () => {
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,10 @@ describe("registerPrompts", () => {
|
||||||
return { server, prompts };
|
return { server, prompts };
|
||||||
}
|
}
|
||||||
|
|
||||||
it("registers 5 prompts", () => {
|
it("registers 4 prompts", () => {
|
||||||
const { server, prompts } = createMockServer();
|
const { server, prompts } = createMockServer();
|
||||||
registerPrompts(server);
|
registerPrompts(server);
|
||||||
expect(prompts.size).toBe(5);
|
expect(prompts.size).toBe(4);
|
||||||
expect(prompts.has("presentation_guide")).toBe(true);
|
|
||||||
expect(prompts.has("search_movie")).toBe(true);
|
expect(prompts.has("search_movie")).toBe(true);
|
||||||
expect(prompts.has("search_show")).toBe(true);
|
expect(prompts.has("search_show")).toBe(true);
|
||||||
expect(prompts.has("whats_new")).toBe(true);
|
expect(prompts.has("whats_new")).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue