style: add prettier and format codebase
This commit is contained in:
parent
bf459740fe
commit
2f58ac7bf8
18 changed files with 151 additions and 70 deletions
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80
|
||||
}
|
||||
40
README.md
40
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
|
||||
|
||||
|
|
|
|||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) }],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -110,10 +110,13 @@ describe("TorrentClawClient cache integration", () => {
|
|||
(globalThis.fetch as ReturnType<typeof vi.fn>)
|
||||
.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();
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<string, string>,
|
||||
) => { messages: { role: string; content: { type: string; text: string } }[] };
|
||||
type PromptHandler = (params: Record<string, string>) => {
|
||||
messages: { role: string; content: { type: string; text: string } }[];
|
||||
};
|
||||
|
||||
describe("registerPrompts", () => {
|
||||
function createMockServer() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ function createMockClient(overrides: Partial<TorrentClawClient> = {}) {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue