Skip to content

Commit e8a60e1

Browse files
Merge pull request #54 from justinhartman/feat/add-server-selection-persistence-mechanism
fix: persist server preference
2 parents e7e8bc6 + 25e9483 commit e8a60e1

16 files changed

+1723
-4566
lines changed

.bun-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.3.0

.github/workflows/develop-workflow.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,14 @@ jobs:
1919
uses: actions/checkout@v5
2020
with:
2121
fetch-depth: 0
22-
- name: Setup Node.js
23-
uses: actions/setup-node@v4
22+
- name: Setup Bun package manager
23+
uses: oven-sh/setup-bun@v2
2424
with:
25-
node-version: '22.x'
26-
cache: 'yarn'
25+
bun-version-file: ".bun-version"
2726
- name: Install dependencies
28-
run: yarn install --frozen-lockfile
27+
run: bun install --frozen-lockfile
2928
- name: Run tests with coverage
30-
run: yarn test:coverage
29+
run: bun test:coverage
3130
- name: Upload coverage to Codecov
3231
uses: codecov/codecov-action@v5
3332
with:

.github/workflows/main-workflow.yml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ jobs:
1717
uses: actions/checkout@v5
1818
with:
1919
fetch-depth: 0
20-
- name: Setup Node.js
21-
uses: actions/setup-node@v4
20+
- name: Setup Bun package manager
21+
uses: oven-sh/setup-bun@v2
2222
with:
23-
node-version: '22.x'
24-
cache: 'yarn'
25-
- name: Install dependencies
26-
run: yarn install --frozen-lockfile
23+
bun-version-file: ".bun-version"
24+
- name: Install production dependencies
25+
run: bun install --production --frozen-lockfile
2726
- name: Run tests with coverage
28-
run: yarn test:coverage
27+
run: bun test:coverage
2928
- name: Upload coverage to Codecov
3029
uses: codecov/codecov-action@v5
3130
with:

Gruntfile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ module.exports = function (grunt) {
7878
grep: '^yarn',
7979
},
8080
{
81-
title: 'NPM Package Updates',
82-
grep: '^npm',
81+
title: 'Bun Package Updates',
82+
grep: '^bun',
8383
},
8484
{
8585
title: 'Branches Merged',

bun.lock

Lines changed: 1435 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controllers/appController.multidomain.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('controllers/appController with MULTI_DOMAIN', () => {
5454
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=1',
5555
server2Src: 'https://multi/?video_id=tt&s=1&e=1',
5656
currentServer: '2',
57+
serverPreferenceKey: 'preferredServer',
5758
})
5859
);
5960
});
@@ -71,6 +72,47 @@ describe('controllers/appController with MULTI_DOMAIN', () => {
7172
server1Src: 'https://domain/embed/movie/tt',
7273
server2Src: 'https://multi/?video_id=tt',
7374
currentServer: '2',
75+
serverPreferenceKey: 'preferredServer',
76+
})
77+
);
78+
});
79+
80+
test('getView honours preferred server cookie', async () => {
81+
(fetchOmdbData as jest.Mock).mockResolvedValue({});
82+
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: false });
83+
const req: any = {
84+
params: { q: '', id: 'tt', type: 'movie' },
85+
user: { id: 'u1' },
86+
headers: { cookie: 'preferredServer=1' },
87+
};
88+
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
89+
await appController.getView(req, res, jest.fn());
90+
expect(res.render).toHaveBeenCalledWith(
91+
'view',
92+
expect.objectContaining({
93+
currentServer: '1',
94+
iframeSrc: 'https://domain/embed/movie/tt',
95+
serverPreferenceKey: 'preferredServer',
96+
})
97+
);
98+
});
99+
100+
test('getView parses cookie with additional entries', async () => {
101+
(fetchOmdbData as jest.Mock).mockResolvedValue({});
102+
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: false });
103+
const req: any = {
104+
params: { q: '', id: 'tt', type: 'movie' },
105+
user: { id: 'u1' },
106+
headers: { cookie: 'foo=bar; preferredServer=2' },
107+
};
108+
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
109+
await appController.getView(req, res, jest.fn());
110+
expect(res.render).toHaveBeenCalledWith(
111+
'view',
112+
expect.objectContaining({
113+
currentServer: '2',
114+
iframeSrc: 'https://multi/?video_id=tt',
115+
serverPreferenceKey: 'preferredServer',
74116
})
75117
);
76118
});

controllers/appController.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ describe('controllers/appController', () => {
142142
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
143143
server2Src: '',
144144
currentServer: '1',
145+
serverPreferenceKey: 'preferredServer',
145146
}));
146147
});
147148

@@ -162,6 +163,7 @@ describe('controllers/appController', () => {
162163
expect(res.render).toHaveBeenCalledWith('view', expect.objectContaining({
163164
season: '1',
164165
episode: '1',
166+
serverPreferenceKey: 'preferredServer',
165167
}));
166168
});
167169

@@ -186,6 +188,7 @@ describe('controllers/appController', () => {
186188
server1Src: 'https://domain/embed/movie/tt',
187189
server2Src: '',
188190
currentServer: '1',
191+
serverPreferenceKey: 'preferredServer',
189192
})
190193
);
191194
});
@@ -200,7 +203,7 @@ describe('controllers/appController', () => {
200203

201204
expect(res.render).toHaveBeenCalledWith(
202205
'view',
203-
expect.objectContaining({ type: 'movie', watched: false })
206+
expect.objectContaining({ type: 'movie', watched: false, serverPreferenceKey: 'preferredServer' })
204207
);
205208
});
206209

@@ -231,7 +234,7 @@ describe('controllers/appController', () => {
231234
);
232235
expect(res.render).toHaveBeenCalledWith(
233236
'view',
234-
expect.objectContaining({ season: '1', episode: '1' })
237+
expect.objectContaining({ season: '1', episode: '1', serverPreferenceKey: 'preferredServer' })
235238
);
236239
});
237240

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

controllers/appController.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { getLatest, invalidateLatest, setLatest } from '../helpers/cache';
2121
import http from '../helpers/httpClient';
2222
import type { AuthRequest } from '../types/interfaces';
2323

24+
const SERVER_PREF_COOKIE = 'preferredServer';
25+
2426
/**
2527
* @namespace appController
2628
* @description Controller object containing methods for handling web application routes and views.
@@ -133,6 +135,13 @@ const appController = {
133135
const id = req.params.id;
134136
const type = req.params.type as 'movie' | 'series';
135137

138+
const cookieHeader =
139+
typeof req.headers?.cookie === 'string' ? req.headers.cookie : '';
140+
const match = cookieHeader.match(
141+
new RegExp(`(?:^|;\\s*)${SERVER_PREF_COOKIE}=(1|2)(?:;|$)`)
142+
);
143+
const preferredServer = match ? (match[1] as '1' | '2') : undefined;
144+
136145
if (type === 'series') {
137146
let season = req.params.season;
138147
let episode = req.params.episode;
@@ -151,12 +160,11 @@ const appController = {
151160
await upsertSeriesProgress(req.user.id, id, season, episode);
152161
}
153162

154-
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(
155-
id,
156-
'series',
163+
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'series', {
157164
season,
158-
episode
159-
);
165+
episode,
166+
preferredServer,
167+
});
160168
const canonical = buildCanonical(res.locals.APP_URL, id, type, season, episode);
161169
const data = await fetchOmdbData(id, false);
162170
const seriesDetail = await getSeriesDetail(id, Number(season));
@@ -167,6 +175,7 @@ const appController = {
167175
server1Src,
168176
server2Src,
169177
currentServer,
178+
serverPreferenceKey: SERVER_PREF_COOKIE,
170179
query,
171180
id,
172181
type,
@@ -185,7 +194,9 @@ const appController = {
185194
watched = history?.watched || false;
186195
}
187196

188-
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'movie');
197+
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'movie', {
198+
preferredServer,
199+
});
189200
const canonical = buildCanonical(res.locals.APP_URL, id, type);
190201
const data = await fetchOmdbData(id, false);
191202

@@ -195,6 +206,7 @@ const appController = {
195206
server1Src,
196207
server2Src,
197208
currentServer,
209+
serverPreferenceKey: SERVER_PREF_COOKIE,
198210
query,
199211
id,
200212
type,

helpers/appHelper.multidomain.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { buildSources } from './appHelper';
3939

4040
describe('appHelper buildSources with MULTI_DOMAIN', () => {
4141
test('series sources prefer multi domain', () => {
42-
const result = buildSources('tt123', 'series', '1', '5');
42+
const result = buildSources('tt123', 'series', {season: '1', episode: '5'});
4343
expect(result.server2Src).toBe('https://multi.example/?video_id=tt123&s=1&e=5');
4444
expect(result.currentServer).toBe('2');
4545
});
@@ -49,4 +49,14 @@ describe('appHelper buildSources with MULTI_DOMAIN', () => {
4949
expect(result.server2Src).toBe('https://multi.example/?video_id=tt123');
5050
expect(result.currentServer).toBe('2');
5151
});
52+
53+
test('preferred server overrides default when available', () => {
54+
const result = buildSources('tt123', 'series', {
55+
season: '1',
56+
episode: '5',
57+
preferredServer: '1',
58+
});
59+
expect(result.currentServer).toBe('1');
60+
expect(result.iframeSrc).toBe('https://domain/embed/tv?imdb=tt123&season=1&episode=5');
61+
});
5262
});

helpers/appHelper.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,19 @@ describe('helpers/appHelper', () => {
187187
expect(sources.currentServer).toBe('1');
188188
});
189189

190+
test('buildSources fills blank season and episode for series without params', () => {
191+
const sources = helper.buildSources('tt-series', 'series');
192+
expect(sources.server1Src).toBe('https://domain/embed/tv?imdb=tt-series&season=&episode=');
193+
expect(sources.server2Src).toBe('');
194+
expect(sources.currentServer).toBe('1');
195+
expect(sources.iframeSrc).toBe('https://domain/embed/tv?imdb=tt-series&season=&episode=');
196+
});
197+
190198
test('buildSources prefers MULTI_DOMAIN sources when configured', () => {
191199
jest.isolateModules(() => {
192200
mockAppConfig.MULTI_DOMAIN = 'multi.example';
193201
const mod = require('./appHelper');
194-
const seriesSources = mod.buildSources('tt2', 'series', '1', '3');
202+
const seriesSources = mod.buildSources('tt2', 'series', {season: '1', episode: '3'});
195203
expect(seriesSources.server2Src).toBe('https://multi.example/?video_id=tt2&s=1&e=3');
196204
expect(seriesSources.currentServer).toBe('2');
197205
const movieSources = mod.buildSources('tt2', 'movie');
@@ -201,6 +209,35 @@ describe('helpers/appHelper', () => {
201209
mockAppConfig.MULTI_DOMAIN = '';
202210
});
203211

212+
test('buildSources honours preferred server when provided', () => {
213+
jest.isolateModules(() => {
214+
mockAppConfig.MULTI_DOMAIN = 'multi.example';
215+
const mod = require('./appHelper');
216+
const movieSources = mod.buildSources('tt2', 'movie', {preferredServer: '1'});
217+
expect(movieSources.currentServer).toBe('1');
218+
expect(movieSources.iframeSrc).toBe('https://domain/embed/movie/tt2');
219+
const seriesSources = mod.buildSources('tt2', 'series', {
220+
season: '2',
221+
episode: '5',
222+
preferredServer: '1',
223+
});
224+
expect(seriesSources.currentServer).toBe('1');
225+
expect(seriesSources.iframeSrc).toBe('https://domain/embed/tv?imdb=tt2&season=2&episode=5');
226+
});
227+
mockAppConfig.MULTI_DOMAIN = '';
228+
});
229+
230+
test('buildSources keeps preferred server 2 when provided', () => {
231+
jest.isolateModules(() => {
232+
mockAppConfig.MULTI_DOMAIN = 'multi.example';
233+
const mod = require('./appHelper');
234+
const movieSources = mod.buildSources('tt2', 'movie', {preferredServer: '2'});
235+
expect(movieSources.currentServer).toBe('2');
236+
expect(movieSources.iframeSrc).toBe('https://multi.example/?video_id=tt2');
237+
});
238+
mockAppConfig.MULTI_DOMAIN = '';
239+
});
240+
204241
test('useAuth is false when no mongo uri', () => {
205242
expect(helper.useAuth).toBe(false);
206243
});

0 commit comments

Comments
 (0)