Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use bun run when invoking tests in develop workflow

The develop CI job executes bun test:coverage, but the test:coverage entry is a package script, not a Bun test file. Without bun run the command fails before Jest starts, so the coverage job will break. Switch the step to bun run test:coverage (or call npx jest directly).

Useful? React with 👍 / 👎.

- 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Correct bun command in main workflow tests

The production workflow uses the same bun test:coverage invocation, which likewise fails because Bun does not execute package scripts without the run subcommand. This will prevent the main branch pipeline from running tests. Replace with bun run test:coverage or another working command.

Useful? React with 👍 / 👎.

- 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
Loading