feat(plugins): trakt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
06a59e1b5c
commit
59ccc90bc1
@@ -0,0 +1,123 @@
|
||||
import { parsePartialDate } from "@sttracker/plugin-sdk/partial-date";
|
||||
import type { MediaType, SourceRecord, WorkHint } from "@sttracker/plugin-sdk/types";
|
||||
|
||||
interface Ids { trakt?: number; slug?: string; imdb?: string | null; tmdb?: number | null; tvdb?: number | null }
|
||||
interface TMovie { title: string; year?: number | null; ids: Ids }
|
||||
interface TShow { title: string; year?: number | null; ids: Ids }
|
||||
interface TSeason { number: number; ids?: Ids }
|
||||
interface TEpisode { season: number; number: number; title?: string | null; ids?: Ids }
|
||||
export interface TItem {
|
||||
type: string;
|
||||
id?: number;
|
||||
watched_at?: string;
|
||||
rating?: number;
|
||||
notes?: string | null;
|
||||
movie?: TMovie;
|
||||
show?: TShow;
|
||||
season?: TSeason;
|
||||
episode?: TEpisode;
|
||||
}
|
||||
|
||||
function ids(map: Record<string, unknown>): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(map)) if (v !== undefined && v !== null && v !== "") out[k] = String(v);
|
||||
return out;
|
||||
}
|
||||
|
||||
function movieHint(m: TMovie): WorkHint {
|
||||
return {
|
||||
title: m.title,
|
||||
mediaType: "movie",
|
||||
...(m.year ? { year: m.year } : {}),
|
||||
externalIds: ids({ trakt: m.ids.trakt, tmdb_movie: m.ids.tmdb, imdb: m.ids.imdb }),
|
||||
};
|
||||
}
|
||||
|
||||
function showHint(s: TShow): WorkHint {
|
||||
return {
|
||||
title: s.title,
|
||||
mediaType: "show",
|
||||
...(s.year ? { year: s.year } : {}),
|
||||
externalIds: ids({ tmdb_show: s.ids.tmdb, tvdb: s.ids.tvdb, imdb: s.ids.imdb, trakt: s.ids.trakt }),
|
||||
};
|
||||
}
|
||||
|
||||
// Season and episode ids are deliberately omitted: their numeric namespaces (tvdb, tmdb, imdb, trakt)
|
||||
// are shared with shows/movies, so they are identified by parent chain + position instead.
|
||||
function seasonHint(show: TShow, n: number): WorkHint {
|
||||
return { title: `Season ${n}`, mediaType: "season", externalIds: {}, parent: showHint(show), position: { season: n } };
|
||||
}
|
||||
|
||||
function episodeHint(show: TShow, e: TEpisode): WorkHint {
|
||||
return {
|
||||
title: e.title || `Episode ${e.number}`,
|
||||
mediaType: "episode",
|
||||
externalIds: {},
|
||||
parent: seasonHint(show, e.season),
|
||||
position: { season: e.season, episode: e.number },
|
||||
};
|
||||
}
|
||||
|
||||
/** Maps a Trakt item (history, rating, watchlist or list item) to a WorkHint, or null if unsupported. */
|
||||
export function hintFor(item: TItem): WorkHint | null {
|
||||
switch (item.type) {
|
||||
case "movie": return item.movie ? movieHint(item.movie) : null;
|
||||
case "show": return item.show ? showHint(item.show) : null;
|
||||
case "season": return item.show && item.season ? seasonHint(item.show, item.season.number) : null;
|
||||
case "episode": return item.show && item.episode ? episodeHint(item.show, item.episode) : null;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function traktIdOf(item: TItem): number | undefined {
|
||||
switch (item.type) {
|
||||
case "movie": return item.movie?.ids.trakt;
|
||||
case "show": return item.show?.ids.trakt;
|
||||
case "season": return item.season?.ids?.trakt;
|
||||
case "episode": return item.episode?.ids?.trakt;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function historyRecord(item: TItem): SourceRecord | null {
|
||||
const work = hintFor(item);
|
||||
if (!work || item.id === undefined) return null;
|
||||
const finishedAt = parsePartialDate(item.watched_at);
|
||||
return { kind: "engagement", key: `history:${item.id}`, raw: item, work, engagement: { finishedAt } };
|
||||
}
|
||||
|
||||
export function ratingRecord(item: TItem): SourceRecord | null {
|
||||
const work = hintFor(item);
|
||||
const tid = traktIdOf(item);
|
||||
if (!work || tid === undefined || typeof item.rating !== "number") return null;
|
||||
return { kind: "entry", key: `rating:${item.type}:${tid}`, raw: item, work, entry: { rating: item.rating * 10 } };
|
||||
}
|
||||
|
||||
export function watchlistRecord(item: TItem): SourceRecord | null {
|
||||
const work = hintFor(item);
|
||||
const tid = traktIdOf(item);
|
||||
if (!work || tid === undefined) return null;
|
||||
return { kind: "entry", key: `watchlist:${item.type}:${tid}`, raw: item, work, entry: { status: "planned" } };
|
||||
}
|
||||
|
||||
export interface TList { name: string; description?: string | null; ids: Ids }
|
||||
|
||||
export function listRecord(list: TList, items: TItem[]): SourceRecord {
|
||||
const mapped: { work: WorkHint; note?: string }[] = [];
|
||||
for (const it of items) {
|
||||
const work = hintFor(it);
|
||||
if (work) mapped.push({ work, ...(it.notes ? { note: it.notes } : {}) });
|
||||
}
|
||||
return {
|
||||
kind: "list",
|
||||
key: `list:${list.ids.trakt}`,
|
||||
raw: { list, items },
|
||||
list: { name: list.name, ...(list.description ? { description: list.description } : {}), items: mapped },
|
||||
};
|
||||
}
|
||||
|
||||
export function searchHint(r: { type: string; movie?: TMovie; show?: TShow }): WorkHint | null {
|
||||
return hintFor({ type: r.type, ...(r.movie ? { movie: r.movie } : {}), ...(r.show ? { show: r.show } : {}) });
|
||||
}
|
||||
|
||||
export type { MediaType };
|
||||
Reference in New Issue
Block a user