Skip to content
Merged
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
1 change: 1 addition & 0 deletions .bun-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.3.0
11 changes: 5 additions & 6 deletions .github/workflows/develop-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup Bun package manager
uses: oven-sh/setup-bun@v2
with:
node-version: '22.x'
cache: 'yarn'
bun-version-file: ".bun-version"
- name: Install dependencies
run: yarn install --frozen-lockfile
run: bun install --frozen-lockfile
- name: Run tests with coverage
run: yarn test:coverage
run: bun test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down
13 changes: 6 additions & 7 deletions .github/workflows/main-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Setup Bun package manager
uses: oven-sh/setup-bun@v2
with:
node-version: '22.x'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
bun-version-file: ".bun-version"
- name: Install production dependencies
run: bun install --production --frozen-lockfile
- name: Run tests with coverage
run: yarn test:coverage
run: bun test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down
4 changes: 2 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ module.exports = function (grunt) {
grep: '^yarn',
},
{
title: 'NPM Package Updates',
grep: '^npm',
title: 'Bun Package Updates',
grep: '^bun',
},
{
title: 'Branches Merged',
Expand Down
1,435 changes: 1,435 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions controllers/appController.multidomain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ 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',
serverPreferenceKey: 'preferredServer',
})
);
});
Expand All @@ -71,6 +72,47 @@ describe('controllers/appController with MULTI_DOMAIN', () => {
server1Src: 'https://domain/embed/movie/tt',
server2Src: 'https://multi/?video_id=tt',
currentServer: '2',
serverPreferenceKey: 'preferredServer',
})
);
});

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: 'preferredServer=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({
currentServer: '1',
iframeSrc: 'https://domain/embed/movie/tt',
serverPreferenceKey: 'preferredServer',
})
);
});

test('getView parses cookie with additional entries', 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: 'foo=bar; preferredServer=2' },
};
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({
currentServer: '2',
iframeSrc: 'https://multi/?video_id=tt',
serverPreferenceKey: 'preferredServer',
})
);
});
Expand Down
9 changes: 6 additions & 3 deletions controllers/appController.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ describe('controllers/appController', () => {
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
server2Src: '',
currentServer: '1',
serverPreferenceKey: 'preferredServer',
}));
});

Expand All @@ -162,6 +163,7 @@ describe('controllers/appController', () => {
expect(res.render).toHaveBeenCalledWith('view', expect.objectContaining({
season: '1',
episode: '1',
serverPreferenceKey: 'preferredServer',
}));
});

Expand All @@ -186,6 +188,7 @@ describe('controllers/appController', () => {
server1Src: 'https://domain/embed/movie/tt',
server2Src: '',
currentServer: '1',
serverPreferenceKey: 'preferredServer',
})
);
});
Expand All @@ -200,7 +203,7 @@ describe('controllers/appController', () => {

expect(res.render).toHaveBeenCalledWith(
'view',
expect.objectContaining({ type: 'movie', watched: false })
expect.objectContaining({ type: 'movie', watched: false, serverPreferenceKey: 'preferredServer' })
);
});

Expand Down Expand Up @@ -231,7 +234,7 @@ describe('controllers/appController', () => {
);
expect(res.render).toHaveBeenCalledWith(
'view',
expect.objectContaining({ season: '1', episode: '1' })
expect.objectContaining({ season: '1', episode: '1', serverPreferenceKey: 'preferredServer' })
);
});

Expand All @@ -246,7 +249,7 @@ describe('controllers/appController', () => {
expect(History.findOneAndUpdate).not.toHaveBeenCalled();
expect(res.render).toHaveBeenCalledWith(
'view',
expect.objectContaining({ season: '1', episode: '1' })
expect.objectContaining({ season: '1', episode: '1', serverPreferenceKey: 'preferredServer' })
);
});

Expand Down
24 changes: 18 additions & 6 deletions controllers/appController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { getLatest, invalidateLatest, setLatest } from '../helpers/cache';
import http from '../helpers/httpClient';
import type { AuthRequest } from '../types/interfaces';

const SERVER_PREF_COOKIE = 'preferredServer';

/**
* @namespace appController
* @description Controller object containing methods for handling web application routes and views.
Expand Down Expand Up @@ -133,6 +135,13 @@ const appController = {
const id = req.params.id;
const type = req.params.type as 'movie' | 'series';

const cookieHeader =
typeof req.headers?.cookie === 'string' ? req.headers.cookie : '';
const match = cookieHeader.match(
new RegExp(`(?:^|;\\s*)${SERVER_PREF_COOKIE}=(1|2)(?:;|$)`)
);
const preferredServer = match ? (match[1] as '1' | '2') : undefined;

if (type === 'series') {
let season = req.params.season;
let episode = req.params.episode;
Expand All @@ -151,12 +160,11 @@ const appController = {
await upsertSeriesProgress(req.user.id, id, season, episode);
}

const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(
id,
'series',
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'series', {
season,
episode
);
episode,
preferredServer,
});
const canonical = buildCanonical(res.locals.APP_URL, id, type, season, episode);
const data = await fetchOmdbData(id, false);
const seriesDetail = await getSeriesDetail(id, Number(season));
Expand All @@ -167,6 +175,7 @@ const appController = {
server1Src,
server2Src,
currentServer,
serverPreferenceKey: SERVER_PREF_COOKIE,
query,
id,
type,
Expand All @@ -185,7 +194,9 @@ const appController = {
watched = history?.watched || false;
}

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

Expand All @@ -195,6 +206,7 @@ const appController = {
server1Src,
server2Src,
currentServer,
serverPreferenceKey: SERVER_PREF_COOKIE,
query,
id,
type,
Expand Down
12 changes: 11 additions & 1 deletion helpers/appHelper.multidomain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { buildSources } from './appHelper';

describe('appHelper buildSources with MULTI_DOMAIN', () => {
test('series sources prefer multi domain', () => {
const result = buildSources('tt123', 'series', '1', '5');
const result = buildSources('tt123', 'series', {season: '1', episode: '5'});
expect(result.server2Src).toBe('https://multi.example/?video_id=tt123&s=1&e=5');
expect(result.currentServer).toBe('2');
});
Expand All @@ -49,4 +49,14 @@ describe('appHelper buildSources with MULTI_DOMAIN', () => {
expect(result.server2Src).toBe('https://multi.example/?video_id=tt123');
expect(result.currentServer).toBe('2');
});

test('preferred server overrides default when available', () => {
const result = buildSources('tt123', 'series', {
season: '1',
episode: '5',
preferredServer: '1',
});
expect(result.currentServer).toBe('1');
expect(result.iframeSrc).toBe('https://domain/embed/tv?imdb=tt123&season=1&episode=5');
});
});
39 changes: 38 additions & 1 deletion helpers/appHelper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,19 @@ describe('helpers/appHelper', () => {
expect(sources.currentServer).toBe('1');
});

test('buildSources fills blank season and episode for series without params', () => {
const sources = helper.buildSources('tt-series', 'series');
expect(sources.server1Src).toBe('https://domain/embed/tv?imdb=tt-series&season=&episode=');
expect(sources.server2Src).toBe('');
expect(sources.currentServer).toBe('1');
expect(sources.iframeSrc).toBe('https://domain/embed/tv?imdb=tt-series&season=&episode=');
});

test('buildSources prefers MULTI_DOMAIN sources when configured', () => {
jest.isolateModules(() => {
mockAppConfig.MULTI_DOMAIN = 'multi.example';
const mod = require('./appHelper');
const seriesSources = mod.buildSources('tt2', 'series', '1', '3');
const seriesSources = mod.buildSources('tt2', 'series', {season: '1', episode: '3'});
expect(seriesSources.server2Src).toBe('https://multi.example/?video_id=tt2&s=1&e=3');
expect(seriesSources.currentServer).toBe('2');
const movieSources = mod.buildSources('tt2', 'movie');
Expand All @@ -201,6 +209,35 @@ describe('helpers/appHelper', () => {
mockAppConfig.MULTI_DOMAIN = '';
});

test('buildSources honours preferred server when provided', () => {
jest.isolateModules(() => {
mockAppConfig.MULTI_DOMAIN = 'multi.example';
const mod = require('./appHelper');
const movieSources = mod.buildSources('tt2', 'movie', {preferredServer: '1'});
expect(movieSources.currentServer).toBe('1');
expect(movieSources.iframeSrc).toBe('https://domain/embed/movie/tt2');
const seriesSources = mod.buildSources('tt2', 'series', {
season: '2',
episode: '5',
preferredServer: '1',
});
expect(seriesSources.currentServer).toBe('1');
expect(seriesSources.iframeSrc).toBe('https://domain/embed/tv?imdb=tt2&season=2&episode=5');
});
mockAppConfig.MULTI_DOMAIN = '';
});

test('buildSources keeps preferred server 2 when provided', () => {
jest.isolateModules(() => {
mockAppConfig.MULTI_DOMAIN = 'multi.example';
const mod = require('./appHelper');
const movieSources = mod.buildSources('tt2', 'movie', {preferredServer: '2'});
expect(movieSources.currentServer).toBe('2');
expect(movieSources.iframeSrc).toBe('https://multi.example/?video_id=tt2');
});
mockAppConfig.MULTI_DOMAIN = '';
});

test('useAuth is false when no mongo uri', () => {
expect(helper.useAuth).toBe(false);
});
Expand Down
Loading