229 lines
7.6 KiB
TypeScript
229 lines
7.6 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}`;
|
|
|
|
if (t.season != null) {
|
|
const ep =
|
|
t.episode != null ? `E${String(t.episode).padStart(2, "0")}` : "";
|
|
line += ` | S${String(t.season).padStart(2, "0")}${ep}`;
|
|
}
|
|
|
|
line += `\n Info hash: ${t.infoHash}`;
|
|
if (compact) {
|
|
line += `\n Magnet: magnet:?xt=urn:btih:${t.infoHash}`;
|
|
} else if (t.magnetUrl) {
|
|
line += `\n Magnet: ${t.magnetUrl}`;
|
|
}
|
|
if (t.torrentUrl) {
|
|
line += `\n Torrent: ${t.torrentUrl}`;
|
|
}
|
|
|
|
// Audio/subtitle track summary (from scanned torrents)
|
|
if (t.audioTracks && t.audioTracks.length > 0) {
|
|
const langs = t.audioTracks
|
|
.map((a) => a.lang || "?")
|
|
.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
line += `\n Audio: ${langs.join(", ")}`;
|
|
const codecs = t.audioTracks
|
|
.map((a) => a.codec)
|
|
.filter((v): v is string => v != null)
|
|
.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
if (codecs.length > 0) line += ` (${codecs.join(", ")})`;
|
|
}
|
|
if (t.subtitleTracks && t.subtitleTracks.length > 0) {
|
|
const langs = t.subtitleTracks
|
|
.map((s) => s.lang || "?")
|
|
.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
line += `\n Subtitles: ${langs.join(", ")}`;
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
export interface FormatOptions {
|
|
compact?: boolean;
|
|
season?: number;
|
|
episode?: number;
|
|
}
|
|
|
|
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) {
|
|
// Filter torrents by season/episode if specified
|
|
let filteredTorrents = r.torrents;
|
|
if (opts?.season !== undefined) {
|
|
filteredTorrents = filteredTorrents.filter(
|
|
(t) => t.season === opts.season,
|
|
);
|
|
if (opts?.episode !== undefined) {
|
|
filteredTorrents = filteredTorrents.filter(
|
|
(t) => t.episode === opts.episode,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (filteredTorrents.length > 0) {
|
|
const top = filteredTorrents
|
|
.sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0))
|
|
.slice(0, 5);
|
|
const totalMsg =
|
|
filteredTorrents.length !== r.torrents.length
|
|
? `${filteredTorrents.length} matching, ${r.torrents.length} total`
|
|
: `${r.torrents.length} total`;
|
|
lines.push(` Torrents (${totalMsg}, top ${top.length}):`);
|
|
for (const t of top) {
|
|
lines.push(formatTorrent(t, opts?.compact));
|
|
}
|
|
} else {
|
|
const seasonEpStr =
|
|
opts?.episode !== undefined
|
|
? `S${String(opts.season).padStart(2, "0")}E${String(opts.episode).padStart(2, "0")}`
|
|
: `season ${opts?.season}`;
|
|
lines.push(
|
|
` No torrents available for ${seasonEpStr} (${r.torrents.length} torrents available for other seasons)`,
|
|
);
|
|
}
|
|
} else {
|
|
lines.push(" No torrents available");
|
|
}
|
|
|
|
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}`);
|
|
if (r.contentUrl) lines.push(` URL: ${r.contentUrl}`);
|
|
|
|
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 headerParts = [
|
|
`Found ${data.total} results (page ${data.page}, showing ${data.results.length}):`,
|
|
];
|
|
if (data.parsedSeason != null) {
|
|
const ep =
|
|
data.parsedEpisode != null
|
|
? `E${String(data.parsedEpisode).padStart(2, "0")}`
|
|
: "";
|
|
headerParts.push(
|
|
`Detected season/episode: S${String(data.parsedSeason).padStart(2, "0")}${ep}`,
|
|
);
|
|
}
|
|
|
|
const results = data.results.map((r, i) => formatResult(r, i + 1, opts));
|
|
return [...headerParts, "", ...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");
|
|
}
|