Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions controllers/appController.multidomain.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchOmdbData, getSeriesDetail } from '../helpers/appHelper';
import { fetchOmdbData, getSeriesDetail, PREFERRED_SERVER_COOKIE } from '../helpers/appHelper';
import History from '../models/History';

jest.mock('../helpers/appHelper', () => {
Expand Down Expand Up @@ -44,7 +44,11 @@ describe('controllers/appController with MULTI_DOMAIN', () => {

test('getView uses multiembed for series', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
const req: any = { params: { q: '', id: 'tt', type: 'series', season: '1', episode: '1' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series', season: '1', episode: '1' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
await appController.getView(req, res, jest.fn());
expect(res.render).toHaveBeenCalledWith(
Expand All @@ -54,14 +58,19 @@ describe('controllers/appController with MULTI_DOMAIN', () => {
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=1',
server2Src: 'https://multi/?video_id=tt&s=1&e=1',
currentServer: '2',
preferredServerCookie: PREFERRED_SERVER_COOKIE,
})
);
});

test('getView uses multiembed for movie', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: false });
const req: any = { params: { q: '', id: 'tt', type: 'movie' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
await appController.getView(req, res, jest.fn());
expect(res.render).toHaveBeenCalledWith(
Expand All @@ -71,6 +80,29 @@ describe('controllers/appController with MULTI_DOMAIN', () => {
server1Src: 'https://domain/embed/movie/tt',
server2Src: 'https://multi/?video_id=tt',
currentServer: '2',
preferredServerCookie: PREFERRED_SERVER_COOKIE,
})
);
});

test('getView honours preferred server cookie', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: false });
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
user: { id: 'u1' },
headers: { cookie: `${PREFERRED_SERVER_COOKIE}=1` },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
await appController.getView(req, res, jest.fn());
expect(res.render).toHaveBeenCalledWith(
'view',
expect.objectContaining({
iframeSrc: 'https://domain/embed/movie/tt',
server1Src: 'https://domain/embed/movie/tt',
server2Src: 'https://multi/?video_id=tt',
currentServer: '1',
preferredServerCookie: PREFERRED_SERVER_COOKIE,
})
);
});
Expand Down
6 changes: 5 additions & 1 deletion controllers/appController.resume.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ describe('appController getView resume redirect', () => {
});

test('redirects to resume location when available', async () => {
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'user-1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
user: { id: 'user-1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

(getResumeRedirect as jest.Mock).mockResolvedValue('/view/tt/series/5/11');
Expand Down
67 changes: 56 additions & 11 deletions controllers/appController.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import appController from './appController';
import http from '../helpers/httpClient';
import { fetchOmdbData, fetchAndUpdatePosters, getSeriesDetail } from '../helpers/appHelper';
import {
fetchOmdbData,
fetchAndUpdatePosters,
getSeriesDetail,
PREFERRED_SERVER_COOKIE,
} from '../helpers/appHelper';
import History from '../models/History';
import { getLatest, setLatest, invalidateLatest } from '../helpers/cache';

Expand Down Expand Up @@ -124,7 +129,11 @@ describe('controllers/appController', () => {

test('getView renders series view', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
const req: any = { params: { q: '', id: 'tt', type: 'series', season: '1', episode: '2' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series', season: '1', episode: '2' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -142,13 +151,18 @@ describe('controllers/appController', () => {
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
server2Src: '',
currentServer: '1',
preferredServerCookie: PREFERRED_SERVER_COOKIE,
}));
});

test('getView defaults season and episode when missing', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOne as jest.Mock).mockResolvedValue(undefined);
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -168,7 +182,11 @@ describe('controllers/appController', () => {
test('getView renders movie view', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: true });
const req: any = { params: { q: '', id: 'tt', type: 'movie' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -186,14 +204,19 @@ describe('controllers/appController', () => {
server1Src: 'https://domain/embed/movie/tt',
server2Src: '',
currentServer: '1',
preferredServerCookie: PREFERRED_SERVER_COOKIE,
})
);
});

test('getView handles missing history on movie view', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOneAndUpdate as jest.Mock).mockResolvedValue(null);
const req: any = { params: { q: '', id: 'tt', type: 'movie' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -207,7 +230,11 @@ describe('controllers/appController', () => {
test('getView redirects to history position for series', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOne as jest.Mock).mockResolvedValue({ lastSeason: 5, lastEpisode: 11 });
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -218,7 +245,11 @@ describe('controllers/appController', () => {
test('getView ignores malformed history and uses defaults', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOne as jest.Mock).mockResolvedValue({ lastSeason: 'abc', lastEpisode: null });
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -237,7 +268,10 @@ describe('controllers/appController', () => {

test('getView series without user does not query history', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
const req: any = { params: { q: '', id: 'tt', type: 'series' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand All @@ -253,7 +287,11 @@ describe('controllers/appController', () => {
test('getView propagates errors from History.findOne', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOne as jest.Mock).mockRejectedValue(new Error('fail'));
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'series' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };
const next = jest.fn();

Expand All @@ -265,7 +303,11 @@ describe('controllers/appController', () => {
test('getView propagates errors from History.findOneAndUpdate', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
(History.findOneAndUpdate as jest.Mock).mockRejectedValue(new Error('fail'));
const req: any = { params: { q: '', id: 'tt', type: 'movie' }, user: { id: 'u1' } };
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
user: { id: 'u1' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
const next = jest.fn();

Expand All @@ -276,7 +318,10 @@ describe('controllers/appController', () => {

test('getView movie without user skips history', async () => {
(fetchOmdbData as jest.Mock).mockResolvedValue({});
const req: any = { params: { q: '', id: 'tt', type: 'movie' } };
const req: any = {
params: { q: '', id: 'tt', type: 'movie' },
headers: { cookie: '' },
};
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };

await appController.getView(req, res, jest.fn());
Expand Down
17 changes: 15 additions & 2 deletions controllers/appController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
buildSources,
fetchAndUpdatePosters,
fetchOmdbData,
getPreferredServer,
getResumeRedirect,
getSeriesDetail,
upsertSeriesProgress,
upsertMovieWatched,
PREFERRED_SERVER_COOKIE,
} from '../helpers/appHelper';
import { getLatest, invalidateLatest, setLatest } from '../helpers/cache';
import http from '../helpers/httpClient';
Expand Down Expand Up @@ -133,6 +135,8 @@ const appController = {
const id = req.params.id;
const type = req.params.type as 'movie' | 'series';

const preferredServer = getPreferredServer(req.headers.cookie);

if (type === 'series') {
let season = req.params.season;
let episode = req.params.episode;
Expand All @@ -155,7 +159,8 @@ const appController = {
id,
'series',
season,
episode
episode,
preferredServer
);
const canonical = buildCanonical(res.locals.APP_URL, id, type, season, episode);
const data = await fetchOmdbData(id, false);
Expand All @@ -175,6 +180,7 @@ const appController = {
seriesDetail,
canonical,
user: req.user,
preferredServerCookie: PREFERRED_SERVER_COOKIE,
});
}

Expand All @@ -185,7 +191,13 @@ const appController = {
watched = history?.watched || false;
}

const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'movie');
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(
id,
'movie',
undefined,
undefined,
preferredServer
);
const canonical = buildCanonical(res.locals.APP_URL, id, type);
const data = await fetchOmdbData(id, false);

Expand All @@ -201,6 +213,7 @@ const appController = {
canonical,
user: req.user,
watched,
preferredServerCookie: PREFERRED_SERVER_COOKIE,
});
}),

Expand Down
15 changes: 15 additions & 0 deletions helpers/appHelper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,25 @@ describe('helpers/appHelper', () => {
const movieSources = mod.buildSources('tt2', 'movie');
expect(movieSources.server2Src).toBe('https://multi.example/?video_id=tt2');
expect(movieSources.currentServer).toBe('2');
const preferredMovie = mod.buildSources('tt2', 'movie', undefined, undefined, '1');
expect(preferredMovie.iframeSrc).toBe('https://domain/embed/movie/tt2');
expect(preferredMovie.currentServer).toBe('1');
const preferredSeries = mod.buildSources('tt2', 'series', '1', '3', '1');
expect(preferredSeries.iframeSrc).toBe('https://domain/embed/tv?imdb=tt2&season=1&episode=3');
expect(preferredSeries.currentServer).toBe('1');
});
mockAppConfig.MULTI_DOMAIN = '';
});

test('getPreferredServer reads cookie values safely', () => {
expect(helper.getPreferredServer(undefined)).toBeUndefined();
expect(helper.getPreferredServer('foo=bar')).toBeUndefined();
const cookie = `foo=bar; ${helper.PREFERRED_SERVER_COOKIE}=1; other=value`;
expect(helper.getPreferredServer(cookie)).toBe('1');
const invalid = `${helper.PREFERRED_SERVER_COOKIE}=3`;
expect(helper.getPreferredServer(invalid)).toBeUndefined();
});

test('useAuth is false when no mongo uri', () => {
expect(helper.useAuth).toBe(false);
});
Expand Down
40 changes: 35 additions & 5 deletions helpers/appHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ export const __clearCaches = (): void => {

const useMulti = Boolean(appConfig.MULTI_DOMAIN);

export type ServerChoice = '1' | '2';

export const PREFERRED_SERVER_COOKIE = 'preferredServer';

const sanitizeServerChoice = (value?: string): ServerChoice | undefined => {
return value === '1' || value === '2' ? value : undefined;
};

const extractCookieValue = (cookieHeader: string, key: string): string | undefined => {
return cookieHeader
.split(';')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => part.split('='))
.find(([name]) => name === key)?.[1];
};

const getPreferredServer = (cookieHeader?: string): ServerChoice | undefined => {
if (!cookieHeader) return undefined;
const value = extractCookieValue(cookieHeader, PREFERRED_SERVER_COOKIE);
return sanitizeServerChoice(value);
};

const determineInitialServer = (preferred?: ServerChoice): ServerChoice => {
if (!useMulti) return '1';
return preferred ? preferred : '2';
};

/**
* Constructs parameters object for OMDB API requests.
* @param query - Search term for title search or IMDB ID for specific item lookup
Expand Down Expand Up @@ -210,22 +238,23 @@ const buildSources = (
id: string,
kind: 'movie' | 'series',
season?: string,
episode?: string
episode?: string,
preferredServer?: ServerChoice
) => {
if (kind === 'series') {
const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/tv?imdb=${id}&season=${season}&episode=${episode}`;
const server2Src = useMulti
? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}&s=${season}&e=${episode}`
: '';
const iframeSrc = useMulti ? server2Src : server1Src;
const currentServer = useMulti ? '2' : '1';
const currentServer = determineInitialServer(preferredServer);
const iframeSrc = currentServer === '2' && server2Src ? server2Src : server1Src;
return {server1Src, server2Src, iframeSrc, currentServer};
}

const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/movie/${id}`;
const server2Src = useMulti ? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}` : '';
const iframeSrc = useMulti ? server2Src : server1Src;
const currentServer = useMulti ? '2' : '1';
const currentServer = determineInitialServer(preferredServer);
const iframeSrc = currentServer === '2' && server2Src ? server2Src : server1Src;
return {server1Src, server2Src, iframeSrc, currentServer};
};

Expand Down Expand Up @@ -302,6 +331,7 @@ export {
buildSources,
fetchAndUpdatePosters,
fetchOmdbData,
getPreferredServer,
getResumeRedirect,
getSeriesDetail,
upsertMovieWatched,
Expand Down
Loading