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
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ APP_SECRET=
OMDB_API_KEY=your_api_key_here

## If your video player is not loading try changing the domain below by using one of these domains:
## vidsrc.in, vidsrc.pm, vidsrc.xyz, vidsrc.net
VIDSRC_DOMAIN=vidsrc.in
## vidsrc-embed.ru, vidsrc-embed.su, vidsrcme.su, vsrc.su
VIDSRC_DOMAIN=vidsrcme.su

## Optional alternate video source. When set, the app uses this domain as Server 2.
#MULTI_DOMAIN=multiembed.mov

## Uncomment and change these if you want authentication, profile and watchlist functionality to work.
## Be sure to setup a MongoDB first and add your connection string to the MONGO_URI below.
Expand Down
5 changes: 5 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Repository Guidelines

## Project Structure & Module Organization
- `api/index.ts` boots the Express server and wires middleware; domain logic sits in `controllers/`, `routes/`, `models/`, and `middleware/` to keep concerns isolated.
- Configuration lives in `config/` with environment-driven exports; shared utilities extend `helpers/` and TypeScript declarations in `types/`.
- UI assets are split between `views/` (EJS templates and partials such as `views/partials/card-add.ejs`) and `public/` for static files.
- Data and ops artefacts live in `migrations/`, `docs/`, `system/`, and automated checks under `tests/e2e` and `tests/integration`.

## Build, Test & Development Commands
- `yarn start` launches the production server through `tsx api/index.ts`; use `yarn start:dev` for watch mode with source maps.
- `yarn lint` performs TypeScript type-checking plus `npx ejslint` over templates; run `yarn lint:ts` or `yarn lint:ejs` to isolate either track.
- `yarn db:migrate` and `yarn db:rollback` apply or revert Mongo migrations in `migrations/`; check current state with `yarn db:status`.
- `yarn test` runs the Jest suite; add `:coverage` to emit reports into `coverage/` and update `junit.xml` for CI.

## Coding Style & Naming Conventions
- TypeScript files use 2-space indentation, trailing commas, and TSDoc-style blocks (`/** ... */`) for exported members as seen in `controllers/`.
- Prefer `camelCase` for functions/variables, `PascalCase` for classes and interfaces, and `kebab-case` for EJS partial filenames.
- Keep logic pure in controllers/helpers; place request wiring in routes; export a single default per module when practical.

## Testing Guidelines
- Tests run on Jest with the `ts-jest` preset; name specs `*.spec.ts` or `*.test.ts` under `tests/` to match `jest.config.js`.
- Aim to maintain coverage across `api`, `config`, `controllers`, `helpers`, `middleware`, `models`, and `routes` as enforced by `collectCoverageFrom`.
- Consume supertest for request flows and prefer fixture factories over network calls; refresh snapshots after intentional UI changes.

## Commit & Pull Request Guidelines
- Follow the observed Conventional Commit style (`feat:`, `refactor:`, `fix:`, `yarn:`) with concise, imperative summaries under ~70 characters.
- Group related changes per commit; include migration IDs or view names when helpful.
- Pull requests should link issues, describe behavioural impact, list validation commands (`yarn test`, `yarn db:migrate`), and attach UI screenshots for template updates.

## Security & Configuration Tips
- Never commit `.env`; update `.env.example` when introducing new settings and document defaults in `config/`.
- Rotate API keys when sharing recordings or tests, and prefer local `.env.test` overrides for automation to avoid polluting development data.
7 changes: 7 additions & 0 deletions config/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('config/app', () => {
delete process.env.APP_URL;
delete process.env.VERCEL_URL;
delete process.env.NODE_ENV;
delete process.env.MULTI_DOMAIN;
};

const loadConfig = () => {
Expand Down Expand Up @@ -93,4 +94,10 @@ describe('config/app', () => {
const config = loadConfig();
expect(config.APP_URL).toBe('http://localhost:3000');
});

test('exposes MULTI_DOMAIN when provided', () => {
process.env.MULTI_DOMAIN = 'multi';
const config = loadConfig();
expect(config.MULTI_DOMAIN).toBe('multi');
});
});
8 changes: 5 additions & 3 deletions config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const appUrl: string =
* @property {string} OMDB_API_KEY - The API key for accessing the OMDb API, essential for the application's functionality
* @property {string} OMDB_API_URL - The API endpoint for the OMDb API, defaulting to `http://www.omdbapi.com`
* @property {string} OMDB_IMG_URL - The URL endpoint for fetching images via the OMDb API, defaulting to `http://img.omdbapi.com`
* @property {string} VIDSRC_DOMAIN - The domain used for the vidsrc player, defaulting to `vidsrc.in` but can be overridden via `process.env.VIDSRC_DOMAIN`
* @property {string} VIDSRC_DOMAIN - The domain used for the vidsrc player, defaulting to `vidsrcme.su` but can be overridden via `process.env.VIDSRC_DOMAIN`
*/
const appConfig = () => {
return {
Expand Down Expand Up @@ -94,8 +94,10 @@ const appConfig = () => {
OMDB_API_URL: process.env.OMDB_API_URL || 'http://www.omdbapi.com',
OMDB_IMG_URL: process.env.OMDB_IMG_URL || 'http://img.omdbapi.com',
// The vidsrc player domain has been prone to be taken down. Use one of the following domains if it's not working:
// vidsrc.in, vidsrc.pm, vidsrc.xyz, vidsrc.net
VIDSRC_DOMAIN: process.env.VIDSRC_DOMAIN || 'vidsrc.in',
// vidsrc-embed.ru, vidsrc-embed.su, vidsrcme.su, vsrc.su
VIDSRC_DOMAIN: process.env.VIDSRC_DOMAIN || 'vidsrcme.su',
// Optional multi-server domain for alternate embeds.
MULTI_DOMAIN: process.env.MULTI_DOMAIN,
/* c8 ignore stop */
};
};
Expand Down
77 changes: 77 additions & 0 deletions controllers/appController.multidomain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { fetchOmdbData, getSeriesDetail } from '../helpers/appHelper';
import History from '../models/History';

jest.mock('../helpers/appHelper', () => {
const actual = jest.requireActual('../helpers/appHelper');
return {
...actual,
fetchOmdbData: jest.fn(),
getSeriesDetail: jest.fn(),
};
});

jest.mock('../models/History', () => ({
__esModule: true,
default: {
findOneAndUpdate: jest.fn(),
},
}));

describe('controllers/appController with MULTI_DOMAIN', () => {
let appController: any;

beforeEach(() => {
jest.resetModules();
jest.doMock('../config/app', () => ({
VIDSRC_DOMAIN: 'domain',
MULTI_DOMAIN: 'multi',
APP_URL: 'http://app',
APP_NAME: 'name',
APP_SUBTITLE: '',
APP_DESCRIPTION: '',
}));
appController = require('./appController').default;
(getSeriesDetail as jest.Mock).mockResolvedValue({
totalSeasons: 1,
seasons: [{ season: 1, episodes: [{ episode: 1 }] }],
});
});

afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});

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 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://multi/?video_id=tt&s=1&e=1',
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=1',
server2Src: 'https://multi/?video_id=tt&s=1&e=1',
currentServer: '2',
})
);
});

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 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://multi/?video_id=tt',
server1Src: 'https://domain/embed/movie/tt',
server2Src: 'https://multi/?video_id=tt',
currentServer: '2',
})
);
});
});
41 changes: 41 additions & 0 deletions controllers/appController.resume.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
jest.mock('../helpers/appHelper', () => {
const actual = jest.requireActual('../helpers/appHelper');
return {
...actual,
fetchOmdbData: jest.fn(async () => ({})),
getSeriesDetail: jest.fn(async () => ({ totalSeasons: 1, currentSeason: { season: 1, episodes: [] } })),
getResumeRedirect: jest.fn(async () => '/view/tt/series/5/11'),
upsertSeriesProgress: jest.fn(),
};
});

jest.mock('../models/History', () => ({
__esModule: true,
default: {
findOne: jest.fn(),
findOneAndUpdate: jest.fn(),
},
}));

import appController from './appController';
import { getResumeRedirect, upsertSeriesProgress } from '../helpers/appHelper';
import History from '../models/History';

describe('appController getView resume redirect', () => {
afterEach(() => {
jest.clearAllMocks();
});

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

(getResumeRedirect as jest.Mock).mockResolvedValue('/view/tt/series/5/11');

await appController.getView(req, res, jest.fn());

expect(res.redirect).toHaveBeenCalledWith('/view/tt/series/5/11');
expect(upsertSeriesProgress).not.toHaveBeenCalled();
expect(History.findOneAndUpdate).not.toHaveBeenCalled();
});
});
38 changes: 30 additions & 8 deletions controllers/appController.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ jest.mock('../helpers/httpClient', () => ({
__esModule: true,
default: { get: jest.fn() },
}));
jest.mock('../helpers/appHelper', () => ({
fetchOmdbData: jest.fn(),
fetchAndUpdatePosters: jest.fn(),
getSeriesDetail: jest.fn(),
}));
jest.mock('../helpers/appHelper', () => {
const actual = jest.requireActual('../helpers/appHelper');
return {
...actual,
fetchOmdbData: jest.fn(),
fetchAndUpdatePosters: jest.fn(),
getSeriesDetail: jest.fn(),
};
});

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

jest.mock('../models/History', () => ({
findOne: jest.fn(),
findOneAndUpdate: jest.fn(),
__esModule: true,
default: {
findOne: jest.fn(),
findOneAndUpdate: jest.fn(),
},
}));

jest.mock('../config/app', () => ({
VIDSRC_DOMAIN: 'domain',
MULTI_DOMAIN: undefined,
APP_URL: 'http://app',
APP_NAME: 'name',
APP_SUBTITLE: '',
Expand Down Expand Up @@ -130,6 +138,10 @@ describe('controllers/appController', () => {
season: '1',
episode: '2',
type: 'series',
iframeSrc: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
server1Src: 'https://domain/embed/tv?imdb=tt&season=1&episode=2',
server2Src: '',
currentServer: '1',
}));
});

Expand Down Expand Up @@ -165,7 +177,17 @@ describe('controllers/appController', () => {
{ $set: { type: 'movie', watched: true } },
{ upsert: true, new: true }
);
expect(res.render).toHaveBeenCalledWith('view', expect.objectContaining({ type: 'movie', watched: true }));
expect(res.render).toHaveBeenCalledWith(
'view',
expect.objectContaining({
type: 'movie',
watched: true,
iframeSrc: 'https://domain/embed/movie/tt',
server1Src: 'https://domain/embed/movie/tt',
server2Src: '',
currentServer: '1',
})
);
});

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