mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(xtream): enhance content retrieval by adding content type filtering for Xtream ID lookups #756
Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
d3231151e7
commit
ff605fc024
20 files changed
+462
-44
No files matched your search
@@ -255,15 +255,17 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
|
||||
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
await clearRecentItems(app.mainWindow);
|
||||
// After clearing all items, the content toggle disappears.
|
||||
// All content types are cleared simultaneously, so no toggle switch needed.
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(0);
|
||||
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
|
||||
await clearRecentItems(app.mainWindow, 'Movies');
|
||||
await expect(
|
||||
contentCardByTitle(app.mainWindow, movieTitle)
|
||||
).toHaveCount(0);
|
||||
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
|
||||
await clearRecentItems(app.mainWindow, 'Series');
|
||||
await expect(
|
||||
contentCardByTitle(app.mainWindow, seriesTitle)
|
||||
).toHaveCount(0);
|
||||
@@ -564,10 +566,11 @@ const xtreamCredentials = {
|
||||
password: defaultXtreamPassword,
|
||||
};
|
||||
|
||||
async function clearRecentItems(page: Page): Promise<void> {
|
||||
async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
|
||||
await page
|
||||
.getByRole('button', { name: 'Clear recently viewed for this section' })
|
||||
.getByRole('button', { name: `Clear recently viewed ${typeLabel}` })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
}
|
||||
|
||||
async function closeUnifiedLiveDetail(page: Page): Promise<void> {
|
||||
|
||||
@@ -511,8 +511,17 @@ const electronApi = {
|
||||
ipcRenderer.invoke('DB_CLEAR_PLAYLIST_RECENT_ITEMS', playlistId),
|
||||
dbRemoveRecentItem: (contentId: number, playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_REMOVE_RECENT_ITEM', contentId, playlistId),
|
||||
dbGetContentByXtreamId: (xtreamId: number, playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_CONTENT_BY_XTREAM_ID', xtreamId, playlistId),
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
'DB_GET_CONTENT_BY_XTREAM_ID',
|
||||
xtreamId,
|
||||
playlistId,
|
||||
contentType
|
||||
),
|
||||
dbDeleteAllPlaylists: (operationId?: string) =>
|
||||
ipcRenderer.invoke('DB_DELETE_ALL_PLAYLISTS', operationId),
|
||||
dbCancelOperation: (operationId: string) =>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
const andMock = jest.fn((...conditions: unknown[]) => ({
|
||||
kind: 'and',
|
||||
conditions,
|
||||
}));
|
||||
const eqMock = jest.fn((left: unknown, right: unknown) => ({
|
||||
kind: 'eq',
|
||||
left,
|
||||
right,
|
||||
}));
|
||||
|
||||
jest.mock('drizzle-orm', () => ({
|
||||
and: (...conditions: unknown[]) => andMock(...conditions),
|
||||
asc: jest.fn(),
|
||||
desc: jest.fn(),
|
||||
eq: (left: unknown, right: unknown) => eqMock(left, right),
|
||||
inArray: jest.fn(),
|
||||
or: jest.fn(),
|
||||
sql: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as schema from 'database-schema';
|
||||
import type { AppDatabase } from '../database.types';
|
||||
import { getContentByXtreamId } from './content.operations';
|
||||
|
||||
function createDbMock(result: unknown[] = []) {
|
||||
const limit = jest.fn().mockResolvedValue(result);
|
||||
const where = jest.fn().mockReturnValue({ limit });
|
||||
const innerJoin = jest.fn().mockReturnValue({ where });
|
||||
const from = jest.fn().mockReturnValue({ innerJoin });
|
||||
const select = jest.fn().mockReturnValue({ from });
|
||||
|
||||
return {
|
||||
db: {
|
||||
select,
|
||||
} as unknown as AppDatabase,
|
||||
innerJoin,
|
||||
limit,
|
||||
select,
|
||||
where,
|
||||
};
|
||||
}
|
||||
|
||||
describe('content.operations', () => {
|
||||
beforeEach(() => {
|
||||
andMock.mockClear();
|
||||
eqMock.mockClear();
|
||||
});
|
||||
|
||||
it('adds the content type filter when resolving by xtream ID', async () => {
|
||||
const { db, where } = createDbMock([
|
||||
{
|
||||
title: 'Krypton',
|
||||
type: 'series',
|
||||
xtream_id: 290,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await getContentByXtreamId(
|
||||
db,
|
||||
290,
|
||||
'playlist-1',
|
||||
'series'
|
||||
);
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(schema.content.xtreamId, 290);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
schema.categories.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
expect(eqMock).toHaveBeenCalledWith(schema.content.type, 'series');
|
||||
expect(where.mock.calls[0][0].conditions).toHaveLength(3);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Krypton',
|
||||
type: 'series',
|
||||
xtream_id: 290,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the legacy lookup path when no content type is provided', async () => {
|
||||
const { db, where } = createDbMock();
|
||||
|
||||
await getContentByXtreamId(db, 290, 'playlist-1');
|
||||
|
||||
expect(eqMock).toHaveBeenCalledWith(schema.content.xtreamId, 290);
|
||||
expect(eqMock).toHaveBeenCalledWith(
|
||||
schema.categories.playlistId,
|
||||
'playlist-1'
|
||||
);
|
||||
expect(eqMock).not.toHaveBeenCalledWith(schema.content.type, 'series');
|
||||
expect(where.mock.calls[0][0].conditions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -368,8 +368,18 @@ export async function clearXtreamImportCache(
|
||||
export async function getContentByXtreamId(
|
||||
db: AppDatabase,
|
||||
xtreamId: number,
|
||||
playlistId: string
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) {
|
||||
const conditions = [
|
||||
eq(schema.content.xtreamId, xtreamId),
|
||||
eq(schema.categories.playlistId, playlistId),
|
||||
];
|
||||
|
||||
if (contentType) {
|
||||
conditions.push(eq(schema.content.type, contentType));
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.select(selectContentFields())
|
||||
.from(schema.content)
|
||||
@@ -377,12 +387,7 @@ export async function getContentByXtreamId(
|
||||
schema.categories,
|
||||
eq(schema.content.categoryId, schema.categories.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.content.xtreamId, xtreamId),
|
||||
eq(schema.categories.playlistId, playlistId)
|
||||
)
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.limit(1);
|
||||
|
||||
return result[0] || null;
|
||||
|
||||
@@ -70,9 +70,14 @@ handleWorkerRequest(
|
||||
|
||||
handleWorkerRequest(
|
||||
'DB_GET_CONTENT_BY_XTREAM_ID',
|
||||
(xtreamId: number, playlistId: string) => ({
|
||||
(
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) => ({
|
||||
xtreamId,
|
||||
playlistId,
|
||||
contentType,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -401,11 +401,13 @@ async function executeRequest(message: DbWorkerRequestMessage) {
|
||||
const payload = message.payload as {
|
||||
xtreamId: number;
|
||||
playlistId: string;
|
||||
contentType?: 'live' | 'movie' | 'series';
|
||||
};
|
||||
return getContentByXtreamId(
|
||||
db,
|
||||
payload.xtreamId,
|
||||
payload.playlistId
|
||||
payload.playlistId,
|
||||
payload.contentType
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -277,7 +277,8 @@ declare global {
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) => Promise<XtreamContent | null>;
|
||||
dbGetAppState: (key: string) => Promise<string | null>;
|
||||
dbSetAppState: (
|
||||
|
||||
Reference in new issue
Block a user