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");
}