diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..638625c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "tabWidth": 2, + "printWidth": 80 +} diff --git a/README.md b/README.md index 81a621b..6934ed4 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,29 @@ No API key required. ## Available Tools -| Tool | Description | -|------|-------------| -| `search_content` | Search movies/shows with filters (query, type, genre, year, rating, quality, language, sort). Returns content with torrents and magnet links. | -| `get_popular` | Get popular content ranked by user clicks | -| `get_recent` | Get recently added content | -| `get_watch_providers` | Streaming availability by country (Netflix, Disney+, etc.) | -| `get_credits` | Cast and director for a title | -| `get_torrent_url` | Get .torrent file download URL from info hash | +| Tool | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `search_content` | Search movies/shows with filters (query, type, genre, year, rating, quality, language, sort). Returns content with torrents and magnet links. | +| `get_popular` | Get popular content ranked by user clicks | +| `get_recent` | Get recently added content | +| `get_watch_providers` | Streaming availability by country (Netflix, Disney+, etc.) | +| `get_credits` | Cast and director for a title | +| `get_torrent_url` | Get .torrent file download URL from info hash | ## Resources -| URI | Description | -|-----|-------------| +| URI | Description | +| --------------------- | ----------------------------------------------------- | | `torrentclaw://stats` | Catalog statistics (content/torrent counts by source) | ## Prompts -| Prompt | Description | -|--------|-------------| -| `search_movie` | Search for a movie by title and get torrents + streaming | -| `search_show` | Search for a TV show by title and get torrents | -| `whats_new` | Discover recently added movies and TV shows | -| `where_to_watch` | Find where to stream, rent, or buy a title | +| Prompt | Description | +| ---------------- | -------------------------------------------------------- | +| `search_movie` | Search for a movie by title and get torrents + streaming | +| `search_show` | Search for a TV show by title and get torrents | +| `whats_new` | Discover recently added movies and TV shows | +| `where_to_watch` | Find where to stream, rent, or buy a title | ## Configuration @@ -89,10 +89,10 @@ Point to your own TorrentClaw instance: ## Environment Variables -| Variable | Default | Description | -|----------|---------|-------------| -| `TORRENTCLAW_API_URL` | `https://torrentclaw.com` | Base URL of the TorrentClaw API | -| `TORRENTCLAW_ALLOW_PRIVATE` | `false` | Set to `true` to allow private/localhost URLs (for self-hosted setups) | +| Variable | Default | Description | +| --------------------------- | ------------------------- | ---------------------------------------------------------------------- | +| `TORRENTCLAW_API_URL` | `https://torrentclaw.com` | Base URL of the TorrentClaw API | +| `TORRENTCLAW_ALLOW_PRIVATE` | `false` | Set to `true` to allow private/localhost URLs (for self-hosted setups) | ## Development diff --git a/package-lock.json b/package-lock.json index 0b7d649..d85a262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@vitest/coverage-v8": "^4.0.18", + "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^4.0.0" @@ -2231,6 +2232,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index c7ada40..5d174d6 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@vitest/coverage-v8": "^4.0.18", + "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^4.0.0" diff --git a/src/formatters/content.ts b/src/formatters/content.ts index c3f03bd..177b349 100644 --- a/src/formatters/content.ts +++ b/src/formatters/content.ts @@ -91,9 +91,7 @@ function formatResult( `Stream: ${r.streaming.flatrate.map((p) => p.name).join(", ")}`, ); if (r.streaming.free.length > 0) - providers.push( - `Free: ${r.streaming.free.map((p) => p.name).join(", ")}`, - ); + providers.push(`Free: ${r.streaming.free.map((p) => p.name).join(", ")}`); if (providers.length > 0) lines.push(` ${providers.join(" | ")}`); } @@ -130,7 +128,8 @@ export function formatPopularResults(data: PopularResponse): string { } const header = `Popular content (${data.total} total, page ${data.page}):`; - const hint = "(Use search_content with a title to get torrents and full details)"; + const hint = + "(Use search_content with a title to get torrents and full details)"; const items = data.items.map((item, i) => formatPopularItem(item, i + 1)); return [header, hint, "", ...items].join("\n"); } @@ -152,7 +151,8 @@ export function formatRecentResults(data: RecentResponse): string { } const header = `Recently added content (${data.total} total, page ${data.page}):`; - const hint = "(Use search_content with a title to get torrents and full details)"; + const hint = + "(Use search_content with a title to get torrents and full details)"; const items = data.items.map((item, i) => formatRecentItem(item, i + 1)); return [header, hint, "", ...items].join("\n"); } diff --git a/src/formatters/providers.ts b/src/formatters/providers.ts index 482631f..b822f1d 100644 --- a/src/formatters/providers.ts +++ b/src/formatters/providers.ts @@ -27,9 +27,7 @@ export function formatWatchProviders(data: WatchProvidersResponse): string { ].filter(Boolean) as string[]; if (sections.length === 0) { - lines.push( - ` No watch providers found in ${data.country}.`, - ); + lines.push(` No watch providers found in ${data.country}.`); } else { lines.push(...sections); } diff --git a/src/tools/get-popular.ts b/src/tools/get-popular.ts index d4b1542..891d40d 100644 --- a/src/tools/get-popular.ts +++ b/src/tools/get-popular.ts @@ -28,10 +28,7 @@ export function registerGetPopular( }, async (params) => { try { - const data = await client.getPopular( - params.limit ?? 10, - params.page, - ); + const data = await client.getPopular(params.limit ?? 10, params.page); return { content: [{ type: "text", text: formatPopularResults(data) }], }; diff --git a/src/tools/get-recent.ts b/src/tools/get-recent.ts index 20e02aa..e0cba31 100644 --- a/src/tools/get-recent.ts +++ b/src/tools/get-recent.ts @@ -28,10 +28,7 @@ export function registerGetRecent( }, async (params) => { try { - const data = await client.getRecent( - params.limit ?? 10, - params.page, - ); + const data = await client.getRecent(params.limit ?? 10, params.page); return { content: [{ type: "text", text: formatRecentResults(data) }], }; diff --git a/src/tools/get-watch-providers.ts b/src/tools/get-watch-providers.ts index 792cddf..85712c8 100644 --- a/src/tools/get-watch-providers.ts +++ b/src/tools/get-watch-providers.ts @@ -27,9 +27,7 @@ export function registerGetWatchProviders( "Must be uppercase 2-letter ISO 3166-1 country code", ) .default("US") - .describe( - "ISO 3166-1 country code (e.g. US, ES, GB, DE). Default: US", - ), + .describe("ISO 3166-1 country code (e.g. US, ES, GB, DE). Default: US"), }, async (params) => { try { diff --git a/tests/api-client.test.ts b/tests/api-client.test.ts index 174da2f..038f19d 100644 --- a/tests/api-client.test.ts +++ b/tests/api-client.test.ts @@ -98,7 +98,9 @@ describe("TorrentClawClient", () => { it("constructs torrent download URL", () => { const client = new TorrentClawClient(); - const url = client.getTorrentDownloadUrl("aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e"); + const url = client.getTorrentDownloadUrl( + "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e", + ); expect(url).toBe( "https://torrentclaw.com/api/v1/torrent/aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e", ); diff --git a/tests/cache.test.ts b/tests/cache.test.ts index 87c11f1..5dad10e 100644 --- a/tests/cache.test.ts +++ b/tests/cache.test.ts @@ -110,10 +110,13 @@ describe("TorrentClawClient cache integration", () => { (globalThis.fetch as ReturnType) .mockResolvedValueOnce(new Response("Server error", { status: 500 })) .mockResolvedValueOnce( - new Response(JSON.stringify({ total: 0, page: 1, pageSize: 10, results: [] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), + new Response( + JSON.stringify({ total: 0, page: 1, pageSize: 10, results: [] }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), ); const client = new TorrentClawClient(); diff --git a/tests/config.test.ts b/tests/config.test.ts index b0b996f..8c9a5c2 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -56,15 +56,11 @@ describe("validateApiUrl", () => { }); it("rejects 0.0.0.0", () => { - expect(() => validateApiUrl("http://0.0.0.0")).toThrow( - "private/reserved", - ); + expect(() => validateApiUrl("http://0.0.0.0")).toThrow("private/reserved"); }); it("rejects 10.x.x.x range", () => { - expect(() => validateApiUrl("http://10.0.0.1")).toThrow( - "private/reserved", - ); + expect(() => validateApiUrl("http://10.0.0.1")).toThrow("private/reserved"); }); it("rejects 172.16-31.x.x range", () => { diff --git a/tests/formatters/content.test.ts b/tests/formatters/content.test.ts index 0621200..701d7c0 100644 --- a/tests/formatters/content.test.ts +++ b/tests/formatters/content.test.ts @@ -36,7 +36,8 @@ describe("formatSearchResults", () => { title: "Inception", titleOriginal: "Inception", year: 2010, - overview: "A thief who steals corporate secrets through dream-sharing technology.", + overview: + "A thief who steals corporate secrets through dream-sharing technology.", posterUrl: "https://image.tmdb.org/t/p/w500/poster.jpg", backdropUrl: null, genres: ["Action", "Science Fiction"], @@ -52,7 +53,8 @@ describe("formatSearchResults", () => { sizeBytes: "2147483648", seeders: 847, leechers: 23, - magnetUrl: "magnet:?xt=urn:btih:aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e", + magnetUrl: + "magnet:?xt=urn:btih:aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e", source: "yts", qualityScore: 85, uploadedAt: "2024-03-15T12:00:00Z", @@ -561,7 +563,12 @@ describe("formatSearchResults", () => { describe("formatPopularResults", () => { it("formats empty results", () => { - const response: PopularResponse = { items: [], total: 0, page: 1, pageSize: 10 }; + const response: PopularResponse = { + items: [], + total: 0, + page: 1, + pageSize: 10, + }; expect(formatPopularResults(response)).toContain("No popular content"); }); @@ -593,7 +600,12 @@ describe("formatPopularResults", () => { describe("formatRecentResults", () => { it("formats empty results", () => { - const response: RecentResponse = { items: [], total: 0, page: 1, pageSize: 10 }; + const response: RecentResponse = { + items: [], + total: 0, + page: 1, + pageSize: 10, + }; expect(formatRecentResults(response)).toContain("No recent content"); }); diff --git a/tests/formatters/providers.test.ts b/tests/formatters/providers.test.ts index d65b3a8..88703e8 100644 --- a/tests/formatters/providers.test.ts +++ b/tests/formatters/providers.test.ts @@ -9,14 +9,38 @@ describe("formatWatchProviders", () => { country: "ES", providers: { flatrate: [ - { providerId: 8, name: "Netflix", logo: null, link: null, displayPriority: 1 }, - { providerId: 337, name: "Disney+", logo: null, link: null, displayPriority: 2 }, + { + providerId: 8, + name: "Netflix", + logo: null, + link: null, + displayPriority: 1, + }, + { + providerId: 337, + name: "Disney+", + logo: null, + link: null, + displayPriority: 2, + }, ], rent: [ - { providerId: 2, name: "Apple TV", logo: null, link: null, displayPriority: 1 }, + { + providerId: 2, + name: "Apple TV", + logo: null, + link: null, + displayPriority: 1, + }, ], buy: [ - { providerId: 3, name: "Google Play", logo: null, link: null, displayPriority: 1 }, + { + providerId: 3, + name: "Google Play", + logo: null, + link: null, + displayPriority: 1, + }, ], free: [], }, @@ -66,8 +90,20 @@ describe("formatWatchProviders", () => { country: "GB", providers: { flatrate: [ - { providerId: 2, name: "Second", logo: null, link: null, displayPriority: 20 }, - { providerId: 1, name: "First", logo: null, link: null, displayPriority: 1 }, + { + providerId: 2, + name: "Second", + logo: null, + link: null, + displayPriority: 20, + }, + { + providerId: 1, + name: "First", + logo: null, + link: null, + displayPriority: 1, + }, ], rent: [], buy: [], @@ -89,7 +125,13 @@ describe("formatWatchProviders", () => { rent: [], buy: [], free: [ - { providerId: 100, name: "Tubi", logo: null, link: null, displayPriority: 1 }, + { + providerId: 100, + name: "Tubi", + logo: null, + link: null, + displayPriority: 1, + }, ], }, attribution: "JustWatch", diff --git a/tests/prompts.test.ts b/tests/prompts.test.ts index a10206c..0d74126 100644 --- a/tests/prompts.test.ts +++ b/tests/prompts.test.ts @@ -2,9 +2,9 @@ import { describe, it, expect, vi } from "vitest"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerPrompts } from "../src/prompts.js"; -type PromptHandler = ( - params: Record, -) => { messages: { role: string; content: { type: string; text: string } }[] }; +type PromptHandler = (params: Record) => { + messages: { role: string; content: { type: string; text: string } }[]; +}; describe("registerPrompts", () => { function createMockServer() { diff --git a/tests/tools/get-credits.test.ts b/tests/tools/get-credits.test.ts index 7a10260..7b9d309 100644 --- a/tests/tools/get-credits.test.ts +++ b/tests/tools/get-credits.test.ts @@ -55,7 +55,9 @@ describe("get_credits tool", () => { it("returns isError on ApiError", async () => { const client = createMockClient({ - getCredits: vi.fn().mockRejectedValue(new ApiError(503, "TMDB unavailable")), + getCredits: vi + .fn() + .mockRejectedValue(new ApiError(503, "TMDB unavailable")), }); const { server, getToolHandler } = createMockServer(); registerGetCredits(server, client); diff --git a/tests/tools/get-torrent-url.test.ts b/tests/tools/get-torrent-url.test.ts index 3827722..b28aa6b 100644 --- a/tests/tools/get-torrent-url.test.ts +++ b/tests/tools/get-torrent-url.test.ts @@ -14,8 +14,7 @@ function createMockClient(overrides: Partial = {}) { getTorrentDownloadUrl: vi .fn() .mockImplementation( - (hash: string) => - `https://torrentclaw.com/api/v1/torrent/${hash}`, + (hash: string) => `https://torrentclaw.com/api/v1/torrent/${hash}`, ), ...overrides, } as unknown as TorrentClawClient; diff --git a/tests/tools/get-watch-providers.test.ts b/tests/tools/get-watch-providers.test.ts index 290f155..9944afa 100644 --- a/tests/tools/get-watch-providers.test.ts +++ b/tests/tools/get-watch-providers.test.ts @@ -24,7 +24,13 @@ describe("get_watch_providers tool", () => { country: "ES", providers: { flatrate: [ - { providerId: 8, name: "Netflix", logo: null, link: null, displayPriority: 1 }, + { + providerId: 8, + name: "Netflix", + logo: null, + link: null, + displayPriority: 1, + }, ], rent: [], buy: [], @@ -64,7 +70,9 @@ describe("get_watch_providers tool", () => { it("returns isError on ApiError", async () => { const client = createMockClient({ - getWatchProviders: vi.fn().mockRejectedValue(new ApiError(404, "Not found")), + getWatchProviders: vi + .fn() + .mockRejectedValue(new ApiError(404, "Not found")), }); const { server, getToolHandler } = createMockServer(); registerGetWatchProviders(server, client); @@ -78,7 +86,9 @@ describe("get_watch_providers tool", () => { it("returns isError on generic error", async () => { const client = createMockClient({ - getWatchProviders: vi.fn().mockRejectedValue(new Error("Connection refused")), + getWatchProviders: vi + .fn() + .mockRejectedValue(new Error("Connection refused")), }); const { server, getToolHandler } = createMockServer(); registerGetWatchProviders(server, client);