mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
- A merged playlist gains the browser document's favorites and history; Xtream values for one server entry are combined (lists unioned, maps merged, the server winning conflicts) and written once. - A failed user-data write stays queued and survives a re-read; the page-hide flush waits for a batch still travelling. - The start-up screen lets the sign-in route through whatever its check reports. - The backend refuses IPTVNATOR_ACCESS_TOKEN without server storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
/**
|
|
* How the move to the server combines user data that two places hold for
|
|
* one playlist: the server's value comes first and wins, the browser only
|
|
* adds. Lists are unioned, maps keep the first value of each key.
|
|
*/
|
|
export function combineUserData(values: readonly unknown[]): unknown {
|
|
const present = values.filter((value) => value !== undefined);
|
|
if (present.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (present.every(Array.isArray)) {
|
|
return unionItems(...(present as unknown[][]));
|
|
}
|
|
if (present.every(isPlainObject)) {
|
|
const merged: Record<string, unknown> = {};
|
|
for (const value of present as Record<string, unknown>[]) {
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
if (!(key in merged)) {
|
|
merged[key] = entry;
|
|
}
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
return present[0];
|
|
}
|
|
|
|
/** Items in order of first appearance, without duplicates. */
|
|
export function unionItems<T>(...lists: readonly (readonly T[])[]): T[] {
|
|
const seen = new Set<string>();
|
|
const result: T[] = [];
|
|
for (const list of lists) {
|
|
for (const item of list) {
|
|
const key = itemKey(item);
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
result.push(item);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Content ids or URLs identify favorites and history entries. */
|
|
function itemKey(item: unknown): string {
|
|
if (!isPlainObject(item)) {
|
|
return `value:${JSON.stringify(item)}`;
|
|
}
|
|
for (const field of ['id', 'stream_id', 'series_id', 'movie_id', 'url']) {
|
|
const value = item[field];
|
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
return `${field}:${value}`;
|
|
}
|
|
}
|
|
return `json:${JSON.stringify(item)}`;
|
|
}
|
|
|
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|