mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
feat(epg): support playlist-scoped sources
Add playlist-scoped EPG source support for M3U playlists.
This commit is contained in:
1 parent
36a7ce9f3d
commit
e801028005
71 files changed
+4876
-373
No files matched your search
@@ -68,6 +68,17 @@ describe('playlist.operations', () => {
|
||||
importDate: '2026-04-01T00:00:00.000Z',
|
||||
favorites: JSON.stringify(['channel-1']),
|
||||
recentlyViewed: JSON.stringify([{ id: 'recent-1' }]),
|
||||
epgUrls: JSON.stringify(['https://example.com/enabled.xml']),
|
||||
detectedEpgUrls: JSON.stringify([
|
||||
'https://example.com/enabled.xml',
|
||||
'https://example.com/detected-only.xml',
|
||||
]),
|
||||
manualEpgUrls: JSON.stringify([
|
||||
'https://example.com/manual.xml',
|
||||
]),
|
||||
disabledEpgUrls: JSON.stringify([
|
||||
'https://example.com/disabled.xml',
|
||||
]),
|
||||
autoRefresh: false,
|
||||
url: 'https://example.com/list.m3u',
|
||||
},
|
||||
@@ -84,6 +95,13 @@ describe('playlist.operations', () => {
|
||||
count: 2,
|
||||
favorites: ['channel-1'],
|
||||
recentlyViewed: [{ id: 'recent-1' }],
|
||||
epgUrls: ['https://example.com/enabled.xml'],
|
||||
detectedEpgUrls: [
|
||||
'https://example.com/enabled.xml',
|
||||
'https://example.com/detected-only.xml',
|
||||
],
|
||||
manualEpgUrls: ['https://example.com/manual.xml'],
|
||||
disabledEpgUrls: ['https://example.com/disabled.xml'],
|
||||
url: 'https://example.com/list.m3u',
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -59,6 +59,16 @@ function parseJsonValue<T>(value: string | null | undefined, fallback: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function getStringArrayValue(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
function getFavoriteChannelMatch(
|
||||
channel: Channel,
|
||||
favoritePositions: ReadonlyMap<string, number>
|
||||
@@ -149,6 +159,22 @@ function buildPlaylistRow(
|
||||
origin: getStringValue(playlist.origin),
|
||||
referrer: getStringValue(playlist.referrer),
|
||||
filePath: getStringValue(playlist.filePath),
|
||||
epgUrls:
|
||||
playlist.epgUrls !== undefined
|
||||
? JSON.stringify(getStringArrayValue(playlist.epgUrls))
|
||||
: undefined,
|
||||
detectedEpgUrls:
|
||||
playlist.detectedEpgUrls !== undefined
|
||||
? JSON.stringify(getStringArrayValue(playlist.detectedEpgUrls))
|
||||
: undefined,
|
||||
manualEpgUrls:
|
||||
playlist.manualEpgUrls !== undefined
|
||||
? JSON.stringify(getStringArrayValue(playlist.manualEpgUrls))
|
||||
: undefined,
|
||||
disabledEpgUrls:
|
||||
playlist.disabledEpgUrls !== undefined
|
||||
? JSON.stringify(getStringArrayValue(playlist.disabledEpgUrls))
|
||||
: undefined,
|
||||
autoRefresh: Boolean(playlist.autoRefresh),
|
||||
macAddress: getStringValue(playlist.macAddress),
|
||||
url: type === PLAYLIST_TYPES.STALKER ? (portalUrl ?? url) : url,
|
||||
@@ -170,7 +196,9 @@ function buildPlaylistRow(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAppPlaylist(row: schema.Playlist): Record<string, unknown> {
|
||||
export function parseAppPlaylist(
|
||||
row: schema.Playlist
|
||||
): Record<string, unknown> {
|
||||
const payload = parseJsonValue<Record<string, unknown> | null>(
|
||||
row.payload,
|
||||
null
|
||||
@@ -178,11 +206,28 @@ export function parseAppPlaylist(row: schema.Playlist): Record<string, unknown>
|
||||
const base = payload && typeof payload === 'object' ? payload : {};
|
||||
const favorites = parseJsonValue<unknown[]>(row.favorites, []);
|
||||
const recentlyViewed = parseJsonValue<unknown[]>(row.recentlyViewed, []);
|
||||
const epgUrls = getStringArrayValue(
|
||||
row.epgUrls ? parseJsonValue<unknown[]>(row.epgUrls, []) : base.epgUrls
|
||||
);
|
||||
const detectedEpgUrls = getStringArrayValue(
|
||||
row.detectedEpgUrls
|
||||
? parseJsonValue<unknown[]>(row.detectedEpgUrls, [])
|
||||
: (base.detectedEpgUrls ?? epgUrls)
|
||||
);
|
||||
const manualEpgUrls = getStringArrayValue(
|
||||
row.manualEpgUrls
|
||||
? parseJsonValue<unknown[]>(row.manualEpgUrls, [])
|
||||
: base.manualEpgUrls
|
||||
);
|
||||
const disabledEpgUrls = getStringArrayValue(
|
||||
row.disabledEpgUrls
|
||||
? parseJsonValue<unknown[]>(row.disabledEpgUrls, [])
|
||||
: base.disabledEpgUrls
|
||||
);
|
||||
const importDate =
|
||||
row.importDate ?? row.dateCreated ?? new Date().toISOString();
|
||||
const portalUrl =
|
||||
row.portalUrl ??
|
||||
(row.type === PLAYLIST_TYPES.STALKER ? row.url : null);
|
||||
row.portalUrl ?? (row.type === PLAYLIST_TYPES.STALKER ? row.url : null);
|
||||
const updateDate =
|
||||
row.updateDate ??
|
||||
(row.lastUpdated ? new Date(row.lastUpdated).getTime() : undefined);
|
||||
@@ -191,9 +236,7 @@ export function parseAppPlaylist(row: schema.Playlist): Record<string, unknown>
|
||||
...base,
|
||||
_id: row.id,
|
||||
title:
|
||||
getStringValue(base.title) ??
|
||||
getStringValue(base.name) ??
|
||||
row.name,
|
||||
getStringValue(base.title) ?? getStringValue(base.name) ?? row.name,
|
||||
count: row.count ?? getNumericValue(base.count) ?? 0,
|
||||
importDate: getStringValue(base.importDate) ?? importDate,
|
||||
lastUsage:
|
||||
@@ -206,9 +249,13 @@ export function parseAppPlaylist(row: schema.Playlist): Record<string, unknown>
|
||||
autoRefresh: row.autoRefresh ?? Boolean(base.autoRefresh),
|
||||
url:
|
||||
row.type === PLAYLIST_TYPES.M3U_URL
|
||||
? row.url ?? getStringValue(base.url)
|
||||
? (row.url ?? getStringValue(base.url))
|
||||
: getStringValue(base.url),
|
||||
filePath: row.filePath ?? getStringValue(base.filePath),
|
||||
epgUrls,
|
||||
detectedEpgUrls,
|
||||
manualEpgUrls,
|
||||
disabledEpgUrls,
|
||||
userAgent: row.userAgent ?? getStringValue(base.userAgent),
|
||||
referrer: row.referrer ?? getStringValue(base.referrer),
|
||||
origin: row.origin ?? getStringValue(base.origin),
|
||||
@@ -260,13 +307,10 @@ export async function upsertAppPlaylist(
|
||||
throw new Error('Playlist ID is required for upsert');
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(schema.playlists)
|
||||
.values(row)
|
||||
.onConflictDoUpdate({
|
||||
target: schema.playlists.id,
|
||||
set: row,
|
||||
});
|
||||
await db.insert(schema.playlists).values(row).onConflictDoUpdate({
|
||||
target: schema.playlists.id,
|
||||
set: row,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -289,8 +333,7 @@ export async function upsertAppPlaylists(
|
||||
|
||||
await db.transaction((tx) => {
|
||||
for (const row of rows) {
|
||||
tx
|
||||
.insert(schema.playlists)
|
||||
tx.insert(schema.playlists)
|
||||
.values(row)
|
||||
.onConflictDoUpdate({
|
||||
target: schema.playlists.id,
|
||||
@@ -323,6 +366,10 @@ export async function getAppPlaylistMetas(db: AppDatabase) {
|
||||
origin: schema.playlists.origin,
|
||||
referrer: schema.playlists.referrer,
|
||||
filePath: schema.playlists.filePath,
|
||||
epgUrls: schema.playlists.epgUrls,
|
||||
detectedEpgUrls: schema.playlists.detectedEpgUrls,
|
||||
manualEpgUrls: schema.playlists.manualEpgUrls,
|
||||
disabledEpgUrls: schema.playlists.disabledEpgUrls,
|
||||
autoRefresh: schema.playlists.autoRefresh,
|
||||
macAddress: schema.playlists.macAddress,
|
||||
url: schema.playlists.url,
|
||||
@@ -453,25 +500,29 @@ export async function deletePlaylist(
|
||||
playlistId: string,
|
||||
control?: OperationControl
|
||||
): Promise<{ success: boolean }> {
|
||||
const [favoriteRows, recentlyViewedRows, playbackPositionRows, downloadRows] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select({ id: schema.favorites.id })
|
||||
.from(schema.favorites)
|
||||
.where(eq(schema.favorites.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.recentlyViewed.id })
|
||||
.from(schema.recentlyViewed)
|
||||
.where(eq(schema.recentlyViewed.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.playbackPositions.id })
|
||||
.from(schema.playbackPositions)
|
||||
.where(eq(schema.playbackPositions.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.downloads.id })
|
||||
.from(schema.downloads)
|
||||
.where(eq(schema.downloads.playlistId, playlistId)),
|
||||
]);
|
||||
const [
|
||||
favoriteRows,
|
||||
recentlyViewedRows,
|
||||
playbackPositionRows,
|
||||
downloadRows,
|
||||
] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.favorites.id })
|
||||
.from(schema.favorites)
|
||||
.where(eq(schema.favorites.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.recentlyViewed.id })
|
||||
.from(schema.recentlyViewed)
|
||||
.where(eq(schema.recentlyViewed.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.playbackPositions.id })
|
||||
.from(schema.playbackPositions)
|
||||
.where(eq(schema.playbackPositions.playlistId, playlistId)),
|
||||
db
|
||||
.select({ id: schema.downloads.id })
|
||||
.from(schema.downloads)
|
||||
.where(eq(schema.downloads.playlistId, playlistId)),
|
||||
]);
|
||||
|
||||
const categoryRows = await db
|
||||
.select({ id: schema.categories.id })
|
||||
@@ -543,7 +594,9 @@ export async function deletePlaylist(
|
||||
}
|
||||
|
||||
await checkpointOperation(control);
|
||||
await db.delete(schema.playlists).where(eq(schema.playlists.id, playlistId));
|
||||
await db
|
||||
.delete(schema.playlists)
|
||||
.where(eq(schema.playlists.id, playlistId));
|
||||
await reportOperationProgress(control, {
|
||||
phase: 'deleting-playlist',
|
||||
current: 1,
|
||||
|
||||
Reference in new issue
Block a user