Skip to content

Commit 1638528

Browse files
Merge pull request #52 from justinhartman/develop
feat: Add multi-server player toggle and resume helpers
2 parents 00db910 + ea2b956 commit 1638528

25 files changed

+1660
-918
lines changed

.env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ APP_SECRET=
1717
OMDB_API_KEY=your_api_key_here
1818

1919
## If your video player is not loading try changing the domain below by using one of these domains:
20-
## vidsrc.in, vidsrc.pm, vidsrc.xyz, vidsrc.net
21-
VIDSRC_DOMAIN=vidsrc.in
20+
## vidsrc-embed.ru, vidsrc-embed.su, vidsrcme.su, vsrc.su
21+
VIDSRC_DOMAIN=vidsrcme.su
22+
23+
## Optional alternate video source. When set, the app uses this domain as Server 2.
24+
#MULTI_DOMAIN=multiembed.mov
2225

2326
## Uncomment and change these if you want authentication, profile and watchlist functionality to work.
2427
## Be sure to setup a MongoDB first and add your connection string to the MONGO_URI below.

.idea/inspectionProfiles/Project_Default.xml

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

AGENTS.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `api/index.ts` boots the Express server and wires middleware; domain logic sits in `controllers/`, `routes/`, `models/`, and `middleware/` to keep concerns isolated.
5+
- Configuration lives in `config/` with environment-driven exports; shared utilities extend `helpers/` and TypeScript declarations in `types/`.
6+
- UI assets are split between `views/` (EJS templates and partials such as `views/partials/card-add.ejs`) and `public/` for static files.
7+
- Data and ops artefacts live in `migrations/`, `docs/`, `system/`, and automated checks under `tests/e2e` and `tests/integration`.
8+
9+
## Build, Test & Development Commands
10+
- `yarn start` launches the production server through `tsx api/index.ts`; use `yarn start:dev` for watch mode with source maps.
11+
- `yarn lint` performs TypeScript type-checking plus `npx ejslint` over templates; run `yarn lint:ts` or `yarn lint:ejs` to isolate either track.
12+
- `yarn db:migrate` and `yarn db:rollback` apply or revert Mongo migrations in `migrations/`; check current state with `yarn db:status`.
13+
- `yarn test` runs the Jest suite; add `:coverage` to emit reports into `coverage/` and update `junit.xml` for CI.
14+
15+
## Coding Style & Naming Conventions
16+
- TypeScript files use 2-space indentation, trailing commas, and TSDoc-style blocks (`/** ... */`) for exported members as seen in `controllers/`.
17+
- Prefer `camelCase` for functions/variables, `PascalCase` for classes and interfaces, and `kebab-case` for EJS partial filenames.
18+
- Keep logic pure in controllers/helpers; place request wiring in routes; export a single default per module when practical.
19+
20+
## Testing Guidelines
21+
- Tests run on Jest with the `ts-jest` preset; name specs `*.spec.ts` or `*.test.ts` under `tests/` to match `jest.config.js`.
22+
- Aim to maintain coverage across `api`, `config`, `controllers`, `helpers`, `middleware`, `models`, and `routes` as enforced by `collectCoverageFrom`.
23+
- Consume supertest for request flows and prefer fixture factories over network calls; refresh snapshots after intentional UI changes.
24+
25+
## Commit & Pull Request Guidelines
26+
- Follow the observed Conventional Commit style (`feat:`, `refactor:`, `fix:`, `yarn:`) with concise, imperative summaries under ~70 characters.
27+
- Group related changes per commit; include migration IDs or view names when helpful.
28+
- Pull requests should link issues, describe behavioural impact, list validation commands (`yarn test`, `yarn db:migrate`), and attach UI screenshots for template updates.
29+
30+
## Security & Configuration Tips
31+
- Never commit `.env`; update `.env.example` when introducing new settings and document defaults in `config/`.
32+
- Rotate API keys when sharing recordings or tests, and prefer local `.env.test` overrides for automation to avoid polluting development data.

config/app.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('config/app', () => {
1212
delete process.env.APP_URL;
1313
delete process.env.VERCEL_URL;
1414
delete process.env.NODE_ENV;
15+
delete process.env.MULTI_DOMAIN;
1516
};
1617

1718
const loadConfig = () => {
@@ -93,4 +94,10 @@ describe('config/app', () => {
9394
const config = loadConfig();
9495
expect(config.APP_URL).toBe('http://localhost:3000');
9596
});
97+
98+
test('exposes MULTI_DOMAIN when provided', () => {
99+
process.env.MULTI_DOMAIN = 'multi';
100+
const config = loadConfig();
101+
expect(config.MULTI_DOMAIN).toBe('multi');
102+
});
96103
});

config/app.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const appUrl: string =
6464
* @property {string} OMDB_API_KEY - The API key for accessing the OMDb API, essential for the application's functionality
6565
* @property {string} OMDB_API_URL - The API endpoint for the OMDb API, defaulting to `http://www.omdbapi.com`
6666
* @property {string} OMDB_IMG_URL - The URL endpoint for fetching images via the OMDb API, defaulting to `http://img.omdbapi.com`
67-
* @property {string} VIDSRC_DOMAIN - The domain used for the vidsrc player, defaulting to `vidsrc.in` but can be overridden via `process.env.VIDSRC_DOMAIN`
67+
* @property {string} VIDSRC_DOMAIN - The domain used for the vidsrc player, defaulting to `vidsrcme.su` but can be overridden via `process.env.VIDSRC_DOMAIN`
6868
*/
6969
const appConfig = () => {
7070
return {
@@ -94,8 +94,10 @@ const appConfig = () => {
9494
OMDB_API_URL: process.env.OMDB_API_URL || 'http://www.omdbapi.com',
9595
OMDB_IMG_URL: process.env.OMDB_IMG_URL || 'http://img.omdbapi.com',
9696
// The vidsrc player domain has been prone to be taken down. Use one of the following domains if it's not working:
97-
// vidsrc.in, vidsrc.pm, vidsrc.xyz, vidsrc.net
98-
VIDSRC_DOMAIN: process.env.VIDSRC_DOMAIN || 'vidsrc.in',
97+
// vidsrc-embed.ru, vidsrc-embed.su, vidsrcme.su, vsrc.su
98+
VIDSRC_DOMAIN: process.env.VIDSRC_DOMAIN || 'vidsrcme.su',
99+
// Optional multi-server domain for alternate embeds.
100+
MULTI_DOMAIN: process.env.MULTI_DOMAIN,
99101
/* c8 ignore stop */
100102
};
101103
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { fetchOmdbData, getSeriesDetail } from '../helpers/appHelper';
2+
import History from '../models/History';
3+
4+
jest.mock('../helpers/appHelper', () => {
5+
const actual = jest.requireActual('../helpers/appHelper');
6+
return {
7+
...actual,
8+
fetchOmdbData: jest.fn(),
9+
getSeriesDetail: jest.fn(),
10+
};
11+
});
12+
13+
jest.mock('../models/History', () => ({
14+
__esModule: true,
15+
default: {
16+
findOneAndUpdate: jest.fn(),
17+
},
18+
}));
19+
20+
describe('controllers/appController with MULTI_DOMAIN', () => {
21+
let appController: any;
22+
23+
beforeEach(() => {
24+
jest.resetModules();
25+
jest.doMock('../config/app', () => ({
26+
VIDSRC_DOMAIN: 'domain',
27+
MULTI_DOMAIN: 'multi',
28+
APP_URL: 'http://app',
29+
APP_NAME: 'name',
30+
APP_SUBTITLE: '',
31+
APP_DESCRIPTION: '',
32+
}));
33+
appController = require('./appController').default;
34+
(getSeriesDetail as jest.Mock).mockResolvedValue({
35+
totalSeasons: 1,
36+
seasons: [{ season: 1, episodes: [{ episode: 1 }] }],
37+
});
38+
});
39+
40+
afterEach(() => {
41+
jest.resetModules();
42+
jest.clearAllMocks();
43+
});
44+
45+
test('getView uses multiembed for series', async () => {
46+
(fetchOmdbData as jest.Mock).mockResolvedValue({});
47+
const req: any = { params: { q: '', id: 'tt', type: 'series', season: '1', episode: '1' }, user: { id: 'u1' } };
48+
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
49+
await appController.getView(req, res, jest.fn());
50+
expect(res.render).toHaveBeenCalledWith(
51+
'view',
52+
expect.objectContaining({
53+
iframeSrc: 'https://multi/?video_id=tt&s=1&e=1',
54+
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=1',
55+
server2Src: 'https://multi/?video_id=tt&s=1&e=1',
56+
currentServer: '2',
57+
})
58+
);
59+
});
60+
61+
test('getView uses multiembed for movie', async () => {
62+
(fetchOmdbData as jest.Mock).mockResolvedValue({});
63+
(History.findOneAndUpdate as jest.Mock).mockResolvedValue({ watched: false });
64+
const req: any = { params: { q: '', id: 'tt', type: 'movie' }, user: { id: 'u1' } };
65+
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn() };
66+
await appController.getView(req, res, jest.fn());
67+
expect(res.render).toHaveBeenCalledWith(
68+
'view',
69+
expect.objectContaining({
70+
iframeSrc: 'https://multi/?video_id=tt',
71+
server1Src: 'https://domain/embed/movie/tt',
72+
server2Src: 'https://multi/?video_id=tt',
73+
currentServer: '2',
74+
})
75+
);
76+
});
77+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
jest.mock('../helpers/appHelper', () => {
2+
const actual = jest.requireActual('../helpers/appHelper');
3+
return {
4+
...actual,
5+
fetchOmdbData: jest.fn(async () => ({})),
6+
getSeriesDetail: jest.fn(async () => ({ totalSeasons: 1, currentSeason: { season: 1, episodes: [] } })),
7+
getResumeRedirect: jest.fn(async () => '/view/tt/series/5/11'),
8+
upsertSeriesProgress: jest.fn(),
9+
};
10+
});
11+
12+
jest.mock('../models/History', () => ({
13+
__esModule: true,
14+
default: {
15+
findOne: jest.fn(),
16+
findOneAndUpdate: jest.fn(),
17+
},
18+
}));
19+
20+
import appController from './appController';
21+
import { getResumeRedirect, upsertSeriesProgress } from '../helpers/appHelper';
22+
import History from '../models/History';
23+
24+
describe('appController getView resume redirect', () => {
25+
afterEach(() => {
26+
jest.clearAllMocks();
27+
});
28+
29+
test('redirects to resume location when available', async () => {
30+
const req: any = { params: { q: '', id: 'tt', type: 'series' }, user: { id: 'user-1' } };
31+
const res: any = { locals: { APP_URL: 'http://app' }, render: jest.fn(), redirect: jest.fn() };
32+
33+
(getResumeRedirect as jest.Mock).mockResolvedValue('/view/tt/series/5/11');
34+
35+
await appController.getView(req, res, jest.fn());
36+
37+
expect(res.redirect).toHaveBeenCalledWith('/view/tt/series/5/11');
38+
expect(upsertSeriesProgress).not.toHaveBeenCalled();
39+
expect(History.findOneAndUpdate).not.toHaveBeenCalled();
40+
});
41+
});

controllers/appController.spec.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ jest.mock('../helpers/httpClient', () => ({
88
__esModule: true,
99
default: { get: jest.fn() },
1010
}));
11-
jest.mock('../helpers/appHelper', () => ({
12-
fetchOmdbData: jest.fn(),
13-
fetchAndUpdatePosters: jest.fn(),
14-
getSeriesDetail: jest.fn(),
15-
}));
11+
jest.mock('../helpers/appHelper', () => {
12+
const actual = jest.requireActual('../helpers/appHelper');
13+
return {
14+
...actual,
15+
fetchOmdbData: jest.fn(),
16+
fetchAndUpdatePosters: jest.fn(),
17+
getSeriesDetail: jest.fn(),
18+
};
19+
});
1620

1721
jest.mock('../helpers/cache', () => ({
1822
getLatest: jest.fn(),
@@ -21,12 +25,16 @@ jest.mock('../helpers/cache', () => ({
2125
}));
2226

2327
jest.mock('../models/History', () => ({
24-
findOne: jest.fn(),
25-
findOneAndUpdate: jest.fn(),
28+
__esModule: true,
29+
default: {
30+
findOne: jest.fn(),
31+
findOneAndUpdate: jest.fn(),
32+
},
2633
}));
2734

2835
jest.mock('../config/app', () => ({
2936
VIDSRC_DOMAIN: 'domain',
37+
MULTI_DOMAIN: undefined,
3038
APP_URL: 'http://app',
3139
APP_NAME: 'name',
3240
APP_SUBTITLE: '',
@@ -130,6 +138,10 @@ describe('controllers/appController', () => {
130138
season: '1',
131139
episode: '2',
132140
type: 'series',
141+
iframeSrc: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
142+
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
143+
server2Src: '',
144+
currentServer: '1',
133145
}));
134146
});
135147

@@ -165,7 +177,17 @@ describe('controllers/appController', () => {
165177
{ $set: { type: 'movie', watched: true } },
166178
{ upsert: true, new: true }
167179
);
168-
expect(res.render).toHaveBeenCalledWith('view', expect.objectContaining({ type: 'movie', watched: true }));
180+
expect(res.render).toHaveBeenCalledWith(
181+
'view',
182+
expect.objectContaining({
183+
type: 'movie',
184+
watched: true,
185+
iframeSrc: 'https://domain/embed/movie/tt',
186+
server1Src: 'https://domain/embed/movie/tt',
187+
server2Src: '',
188+
currentServer: '1',
189+
})
190+
);
169191
});
170192

171193
test('getView handles missing history on movie view', async () => {

0 commit comments

Comments
 (0)