158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import type {
|
|
SearchResponse,
|
|
SearchResult,
|
|
TorrentInfo,
|
|
PopularResponse,
|
|
PopularItem,
|
|
RecentResponse,
|
|
RecentItem,
|
|
} from "../types.js";
|
|
|
|
function formatSize(bytes: string | null): string {
|
|
if (!bytes) return "?";
|
|
const b = parseInt(bytes, 10);
|
|
if (isNaN(b)) return "?";
|
|
if (b >= 1_073_741_824) return `${(b / 1_073_741_824).toFixed(1)} GB`;
|
|
if (b >= 1_048_576) return `${(b / 1_048_576).toFixed(0)} MB`;
|
|
return `${(b / 1024).toFixed(0)} KB`;
|
|
}
|
|
|
|
function formatRating(imdb: string | null, tmdb: string | null): string {
|
|
const parts: string[] = [];
|
|
if (imdb) parts.push(`IMDb: ${imdb}`);
|
|
if (tmdb) parts.push(`TMDB: ${tmdb}`);
|
|
return parts.length > 0 ? parts.join(" | ") : "No ratings";
|
|
}
|
|
|
|
function truncate(text: string, max: number): string {
|
|
if (text.length <= max) return text;
|
|
return text.slice(0, max - 3) + "...";
|
|
}
|
|
|
|
function formatTorrent(t: TorrentInfo, compact?: boolean): string {
|
|
const parts: string[] = [];
|
|
if (t.quality) parts.push(t.quality);
|
|
if (t.sourceType) parts.push(t.sourceType);
|
|
if (t.codec) parts.push(t.codec);
|
|
if (t.hdrType) parts.push(t.hdrType);
|
|
const label = parts.length > 0 ? parts.join(" ") : "Unknown quality";
|
|
const size = formatSize(t.sizeBytes);
|
|
const seeds = `${t.seeders} seeders`;
|
|
const score = t.qualityScore !== null ? `Score: ${t.qualityScore}` : null;
|
|
|
|
let line = ` - ${label} (${size}) | ${seeds}`;
|
|
if (score) line += ` | ${score}`;
|
|
line += `\n Info hash: ${t.infoHash}`;
|
|
if (compact) {
|
|
// Short magnet (hash only, no trackers) — still clickable, saves ~200 chars per torrent
|
|
line += `\n Magnet: magnet:?xt=urn:btih:${t.infoHash}`;
|
|
} else if (t.magnetUrl) {
|
|
line += `\n Magnet: ${t.magnetUrl}`;
|
|
}
|
|
return line;
|
|
}
|
|
|
|
export interface FormatOptions {
|
|
compact?: boolean;
|
|
}
|
|
|
|
function formatResult(
|
|
r: SearchResult,
|
|
index: number,
|
|
opts?: FormatOptions,
|
|
): string {
|
|
const lines: string[] = [];
|
|
const yearStr = r.year ? ` (${r.year})` : "";
|
|
lines.push(`${index}. ${r.title}${yearStr} [${r.contentType}]`);
|
|
lines.push(` ${formatRating(r.ratingImdb, r.ratingTmdb)}`);
|
|
if (r.genres && r.genres.length > 0) {
|
|
lines.push(` Genres: ${r.genres.join(", ")}`);
|
|
}
|
|
if (r.overview) {
|
|
lines.push(` ${truncate(r.overview, 200)}`);
|
|
}
|
|
|
|
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));
|
|
}
|
|
} else {
|
|
lines.push(" No torrents available");
|
|
}
|
|
|
|
if (r.streaming) {
|
|
const providers: string[] = [];
|
|
if (r.streaming.flatrate.length > 0)
|
|
providers.push(
|
|
`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(", ")}`);
|
|
if (providers.length > 0) lines.push(` ${providers.join(" | ")}`);
|
|
}
|
|
|
|
lines.push(
|
|
` Content ID: ${r.id} — use with get_watch_providers(content_id=${r.id}) or get_credits(content_id=${r.id})`,
|
|
);
|
|
if (r.imdbId) lines.push(` IMDb: ${r.imdbId}`);
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function formatSearchResults(
|
|
data: SearchResponse,
|
|
opts?: FormatOptions,
|
|
): string {
|
|
if (data.results.length === 0) {
|
|
return "No results found. Try: (1) a shorter or alternate title, (2) removing filters like quality or year, (3) checking spelling. You can also try get_popular or get_recent to browse available content.";
|
|
}
|
|
|
|
const header = `Found ${data.total} results (page ${data.page}, showing ${data.results.length}):`;
|
|
const results = data.results.map((r, i) => formatResult(r, i + 1, opts));
|
|
return [header, "", ...results].join("\n");
|
|
}
|
|
|
|
function formatPopularItem(item: PopularItem, index: number): string {
|
|
const yearStr = item.year ? ` (${item.year})` : "";
|
|
const rating = formatRating(item.ratingImdb, item.ratingTmdb);
|
|
return `${index}. ${item.title}${yearStr} [${item.contentType}] — ${rating} — ${item.clickCount} clicks — ID: ${item.id}`;
|
|
}
|
|
|
|
export function formatPopularResults(data: PopularResponse): string {
|
|
if (data.items.length === 0) {
|
|
return "No popular content found.";
|
|
}
|
|
|
|
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 items = data.items.map((item, i) => formatPopularItem(item, i + 1));
|
|
return [header, hint, "", ...items].join("\n");
|
|
}
|
|
|
|
function formatRecentItem(item: RecentItem, index: number): string {
|
|
const yearStr = item.year ? ` (${item.year})` : "";
|
|
const rating = formatRating(item.ratingImdb, item.ratingTmdb);
|
|
const date = new Date(item.createdAt).toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
return `${index}. ${item.title}${yearStr} [${item.contentType}] — ${rating} — Added: ${date} — ID: ${item.id}`;
|
|
}
|
|
|
|
export function formatRecentResults(data: RecentResponse): string {
|
|
if (data.items.length === 0) {
|
|
return "No recent content found.";
|
|
}
|
|
|
|
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 items = data.items.map((item, i) => formatRecentItem(item, i + 1));
|
|
return [header, hint, "", ...items].join("\n");
|
|
}
|