Files
STTracker/plugins/trakt/src/sync.ts
T
2026-10-30 15:49:47 +01:00

93 lines
3.7 KiB
TypeScript

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<SourceRecord> {
for (let page = 1, pages = 1; page <= pages; page++) {
const r = await get<TItem[]>(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<SourceRecord> {
for (const item of (await get<TItem[]>(ctx, token, "/sync/ratings")).data) {
const rec = ratingRecord(item);
if (rec) yield rec;
}
}
async function* watchlist(ctx: PluginContext, token: string): AsyncGenerator<SourceRecord> {
for (const item of (await get<TItem[]>(ctx, token, "/sync/watchlist")).data) {
const rec = watchlistRecord(item);
if (rec) yield rec;
}
}
async function* lists(ctx: PluginContext, token: string): AsyncGenerator<SourceRecord> {
for (const list of (await get<TList[]>(ctx, token, "/users/me/lists")).data) {
const items = (await get<TItem[]>(ctx, token, `/users/me/lists/${list.ids.trakt}/items`)).data;
yield listRecord(list, items);
}
}
export async function* pull(ctx: PluginContext): AsyncGenerator<SourceRecord> {
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<string>((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<Cursor>;
return { history: c.history ?? "", ratings: c.ratings ?? "", watchlist: c.watchlist ?? "", lists: c.lists ?? "" };
} catch {
return null;
}
}
export async function* pullChanges(ctx: PluginContext): AsyncGenerator<SourceRecord> {
const token = await ensureAccessToken(ctx);
const raw = await ctx.cursor.get();
const prev = parseCursor(raw);
const next = cursorFrom((await get<LastActivities>(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);
}