import type { PluginContext, SourceRecord } from "@sttracker/plugin-sdk/types"; import { ensureAccessToken, get } from "./api.ts"; import { historyRecord, listRecord, ratingRecord, watchlistRecord, type TItem, type TList } from "./map.ts"; const PAGE_LIMIT = 250; async function* history(ctx: PluginContext, token: string, startAt?: string): AsyncGenerator { for (let page = 1, pages = 1; page <= pages; page++) { const r = await get(ctx, token, "/sync/history", { page, limit: PAGE_LIMIT, start_at: startAt }); pages = r.pageCount; for (const item of r.data) { const rec = historyRecord(item); if (rec) yield rec; } } } async function* ratings(ctx: PluginContext, token: string): AsyncGenerator { for (const item of (await get(ctx, token, "/sync/ratings")).data) { const rec = ratingRecord(item); if (rec) yield rec; } } async function* watchlist(ctx: PluginContext, token: string): AsyncGenerator { for (const item of (await get(ctx, token, "/sync/watchlist")).data) { const rec = watchlistRecord(item); if (rec) yield rec; } } async function* lists(ctx: PluginContext, token: string): AsyncGenerator { for (const list of (await get(ctx, token, "/users/me/lists")).data) { const items = (await get(ctx, token, `/users/me/lists/${list.ids.trakt}/items`)).data; yield listRecord(list, items); } } export async function* pull(ctx: PluginContext): AsyncGenerator { const token = await ensureAccessToken(ctx); yield* history(ctx, token); yield* ratings(ctx, token); yield* watchlist(ctx, token); yield* lists(ctx, token); } interface Activity { watched_at?: string | null; rated_at?: string | null; watchlisted_at?: string | null; updated_at?: string | null } interface LastActivities { movies?: Activity; episodes?: Activity; shows?: Activity; seasons?: Activity; lists?: Activity } interface Cursor { history: string; ratings: string; watchlist: string; lists: string } const maxIso = (xs: (string | null | undefined)[]): string => xs.reduce((a, x) => (x && x > a ? x : a), ""); function cursorFrom(a: LastActivities): Cursor { return { history: maxIso([a.movies?.watched_at, a.episodes?.watched_at]), ratings: maxIso([a.movies?.rated_at, a.episodes?.rated_at, a.shows?.rated_at, a.seasons?.rated_at]), watchlist: maxIso([a.movies?.watchlisted_at, a.episodes?.watchlisted_at, a.shows?.watchlisted_at, a.seasons?.watchlisted_at]), lists: maxIso([a.lists?.updated_at]), }; } function parseCursor(s: string | null): Cursor | null { if (!s) return null; try { const c = JSON.parse(s) as Partial; return { history: c.history ?? "", ratings: c.ratings ?? "", watchlist: c.watchlist ?? "", lists: c.lists ?? "" }; } catch { return null; } } export async function* pullChanges(ctx: PluginContext): AsyncGenerator { const token = await ensureAccessToken(ctx); const raw = await ctx.cursor.get(); const prev = parseCursor(raw); const next = cursorFrom((await get(ctx, token, "/sync/last_activities")).data); if (!prev) { // No usable cursor: full pull, then remember where we are. yield* history(ctx, token); yield* ratings(ctx, token); yield* watchlist(ctx, token); yield* lists(ctx, token); } else { if (next.history !== prev.history) yield* history(ctx, token, prev.history || undefined); if (next.ratings !== prev.ratings) yield* ratings(ctx, token); if (next.watchlist !== prev.watchlist) yield* watchlist(ctx, token); if (next.lists !== prev.lists) yield* lists(ctx, token); } const encoded = JSON.stringify(next); if (encoded !== raw) await ctx.cursor.set(encoded); }