fix(xtream): enhance content retrieval by adding content type filtering for Xtream ID lookups #756

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-19 22:33:49 +02:00
1 parent d3231151e7
commit ff605fc024
20 files changed
+462 -44

No files matched your search

@@ -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;