Skip to content

Commit 1f9840d

Browse files
committed
feat: Add server switch buttons to movies & series / add LD meta tags
1 parent 5ae2233 commit 1f9840d

File tree

8 files changed

+285
-98
lines changed

8 files changed

+285
-98
lines changed

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

controllers/appController.ts

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ import asyncHandler from 'express-async-handler';
77
import { Response } from 'express';
88

99
import appConfig from '../config/app';
10-
import { fetchOmdbData, fetchAndUpdatePosters, getSeriesDetail } from '../helpers/appHelper';
10+
import {
11+
buildCanonical,
12+
buildSources,
13+
fetchAndUpdatePosters,
14+
fetchOmdbData,
15+
getResumeRedirect,
16+
getSeriesDetail,
17+
upsertSeriesProgress,
18+
upsertMovieWatched,
19+
} from '../helpers/appHelper';
20+
import { getLatest, invalidateLatest, setLatest } from '../helpers/cache';
1121
import http from '../helpers/httpClient';
12-
import History from '../models/History';
1322
import type { AuthRequest } from '../types/interfaces';
14-
import { getLatest, setLatest, invalidateLatest } from '../helpers/cache';
1523

1624
/**
1725
* @namespace appController
@@ -123,51 +131,36 @@ const appController = {
123131
getView: asyncHandler(async (req: AuthRequest, res: Response) => {
124132
const query = req.params.q || '';
125133
const id = req.params.id;
126-
const type = req.params.type;
134+
const type = req.params.type as 'movie' | 'series';
127135

128136
if (type === 'series') {
129137
let season = req.params.season;
130138
let episode = req.params.episode;
131139

132140
if ((!season || !episode) && req.user) {
133-
const history = await History.findOne({
134-
userId: req.user.id,
135-
imdbId: id,
136-
});
137-
if (history) {
138-
const { lastSeason, lastEpisode } = history as any;
139-
if (
140-
Number.isInteger(lastSeason) &&
141-
Number.isInteger(lastEpisode) &&
142-
lastSeason > 0 &&
143-
lastEpisode > 0
144-
) {
145-
return res.redirect(`/view/${id}/series/${lastSeason}/${lastEpisode}`);
146-
}
141+
const redirectTo = await getResumeRedirect(req.user.id, id);
142+
if (redirectTo) {
143+
return res.redirect(redirectTo);
147144
}
148145
}
149146

150147
season = season || '1';
151148
episode = episode || '1';
152149

153150
if (req.user) {
154-
await History.findOneAndUpdate(
155-
{ userId: req.user.id, imdbId: id },
156-
{ $set: { type: 'series', lastSeason: Number(season), lastEpisode: Number(episode) } },
157-
{ upsert: true }
158-
);
151+
await upsertSeriesProgress(req.user.id, id, season, episode);
159152
}
160153

161-
const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/tv?imdb=${id}&season=${season}&episode=${episode}`;
162-
const server2Src = appConfig.MULTI_DOMAIN
163-
? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}&s=${season}&e=${episode}`
164-
: '';
165-
const useMulti = Boolean(appConfig.MULTI_DOMAIN);
166-
const iframeSrc = useMulti ? server2Src : server1Src;
167-
const currentServer = useMulti ? '2' : '1';
168-
const canonical = `${res.locals.APP_URL}/view/${id}/${type}/${season}/${episode}`;
154+
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(
155+
id,
156+
'series',
157+
season,
158+
episode
159+
);
160+
const canonical = buildCanonical(res.locals.APP_URL, id, type, season, episode);
169161
const data = await fetchOmdbData(id, false);
170162
const seriesDetail = await getSeriesDetail(id, Number(season));
163+
171164
return res.render('view', {
172165
data,
173166
iframeSrc,
@@ -185,23 +178,17 @@ const appController = {
185178
});
186179
}
187180

181+
// movie branch
188182
let watched = false;
189183
if (req.user) {
190-
const history = await History.findOneAndUpdate(
191-
{ userId: req.user.id, imdbId: id },
192-
{ $set: { type: 'movie', watched: true } },
193-
{ upsert: true, new: true }
194-
);
184+
const history = await upsertMovieWatched(req.user.id, id);
195185
watched = history?.watched || false;
196186
}
197187

198-
const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/movie/${id}`;
199-
const server2Src = appConfig.MULTI_DOMAIN ? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}` : '';
200-
const useMulti = Boolean(appConfig.MULTI_DOMAIN);
201-
const iframeSrc = useMulti ? server2Src : server1Src;
202-
const currentServer = useMulti ? '2' : '1';
203-
const canonical = `${res.locals.APP_URL}/view/${id}/${type}`;
188+
const { server1Src, server2Src, iframeSrc, currentServer } = buildSources(id, 'movie');
189+
const canonical = buildCanonical(res.locals.APP_URL, id, type);
204190
const data = await fetchOmdbData(id, false);
191+
205192
res.render('view', {
206193
data,
207194
iframeSrc,

helpers/appHelper.ts

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { AxiosRequestConfig } from 'axios';
1212
import http from './httpClient';
1313
import appConfig from '../config/app';
14+
import History from '../models/History';
1415
import type { EpisodeInfo, SeasonDetail, SeriesDetail } from '../types/interfaces';
1516

1617
/**
@@ -26,6 +27,8 @@ export const __clearCaches = (): void => {
2627
seriesCache.clear();
2728
};
2829

30+
const useMulti = Boolean(appConfig.MULTI_DOMAIN);
31+
2932
/**
3033
* Constructs parameters object for OMDB API requests.
3134
* @param query - Search term for title search or IMDB ID for specific item lookup
@@ -51,7 +54,7 @@ const constructOmdbParams = (query: string, search: boolean, type: string): obje
5154
* For ID lookups: Detailed movie object or {Response: "False", Error: string}
5255
* @throws Will throw an error if the API request fails
5356
*/
54-
export const fetchOmdbData = async (
57+
const fetchOmdbData = async (
5558
query: string,
5659
search = true,
5760
type = ''
@@ -86,7 +89,7 @@ export const fetchOmdbData = async (
8689
* If poster isn't available or request fails, sets default 'no-binger' image.
8790
* Updates are performed in parallel using Promise.all
8891
*/
89-
export const fetchAndUpdatePosters = async (show: any[]): Promise<void> => {
92+
const fetchAndUpdatePosters = async (show: any[]): Promise<void> => {
9093
const fallback = `${appConfig.APP_URL}/images/no-binger.jpg`;
9194
await Promise.all(
9295
show.map(async (x: any) => {
@@ -112,7 +115,7 @@ export const fetchAndUpdatePosters = async (show: any[]): Promise<void> => {
112115
* @param season - Season number to retrieve
113116
* @returns {Promise<SeriesDetail>} Object containing total seasons and cached season details
114117
*/
115-
export const getSeriesDetail = async (id: string, season: number): Promise<SeriesDetail> => {
118+
const getSeriesDetail = async (id: string, season: number): Promise<SeriesDetail> => {
116119
if (!id) return { totalSeasons: 0, currentSeason: { season: 0, episodes: [] } };
117120

118121
const buildOptions = (s: number): AxiosRequestConfig => ({
@@ -171,6 +174,113 @@ export const getSeriesDetail = async (id: string, season: number): Promise<Serie
171174
};
172175
};
173176

177+
/**
178+
* Builds canonical URL for a movie/series.
179+
* @param {string} appUrl - Base application URL
180+
* @param {string} id - IMDB ID of the movie/show
181+
* @param {string} type - Type of content ('movie' or 'series')
182+
* @param {string} [season] - Season number (for series)
183+
* @param {string} [episode] - Episode number (for series)
184+
* @returns {string} Canonical URL for the content
185+
*/
186+
const buildCanonical = (
187+
appUrl: string,
188+
id: string,
189+
type: string,
190+
season?: string,
191+
episode?: string
192+
) => {
193+
return season && episode
194+
? `${appUrl}/view/${id}/${type}/${season}/${episode}`
195+
: `${appUrl}/view/${id}/${type}`;
196+
};
197+
198+
/**
199+
* Builds video source URLs for movie/series iframe embedding.
200+
* @param {string} id - IMDB ID of the movie/show
201+
* @param {'movie' | 'series'} kind - Type of content ('movie' or 'series')
202+
* @param {string} [season] - Season number (required for series)
203+
* @param {string} [episode] - Episode number (required for series)
204+
* @returns {Object} Object containing source URLs and current server
205+
*/
206+
const buildSources = (
207+
id: string,
208+
kind: 'movie' | 'series',
209+
season?: string,
210+
episode?: string
211+
) => {
212+
if (kind === 'series') {
213+
const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/tv?imdb=${id}&season=${season}&episode=${episode}`;
214+
const server2Src = useMulti
215+
? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}&s=${season}&e=${episode}`
216+
: '';
217+
const iframeSrc = useMulti ? server2Src : server1Src;
218+
const currentServer = useMulti ? '2' : '1';
219+
return {server1Src, server2Src, iframeSrc, currentServer};
220+
}
221+
222+
const server1Src = `https://${appConfig.VIDSRC_DOMAIN}/embed/movie/${id}`;
223+
const server2Src = useMulti ? `https://${appConfig.MULTI_DOMAIN}/?video_id=${id}` : '';
224+
const iframeSrc = useMulti ? server2Src : server1Src;
225+
const currentServer = useMulti ? '2' : '1';
226+
return {server1Src, server2Src, iframeSrc, currentServer};
227+
};
228+
229+
/**
230+
* Gets resume redirect URL for a series based on last watched episode.
231+
* @param {string} userId - User ID
232+
* @param {string} imdbId - IMDB ID of the series
233+
* @returns {Promise<string|null>} Redirect URL if valid progress exists, null otherwise
234+
*/
235+
const getResumeRedirect = async (userId: string, imdbId: string): Promise<string | null> => {
236+
const history = await History.findOne({userId, imdbId});
237+
if (!history) return null;
238+
239+
const {lastSeason, lastEpisode} = history as any;
240+
const valid =
241+
Number.isInteger(lastSeason) &&
242+
Number.isInteger(lastEpisode) &&
243+
lastSeason > 0 &&
244+
lastEpisode > 0;
245+
246+
return valid ? `/view/${imdbId}/series/${lastSeason}/${lastEpisode}` : null;
247+
};
248+
249+
/**
250+
* Updates or creates a movie watched record.
251+
* @param {string} userId - User ID
252+
* @param {string} imdbId - IMDB ID of the movie
253+
* @returns {Promise<any>} The updated or created history record
254+
*/
255+
const upsertMovieWatched = async (userId: string, imdbId: string) => {
256+
return History.findOneAndUpdate(
257+
{userId, imdbId},
258+
{$set: {type: 'movie', watched: true}},
259+
{upsert: true, new: true}
260+
);
261+
};
262+
263+
/**
264+
* Updates or creates a series progress record.
265+
* @param {string} userId - User ID
266+
* @param {string} imdbId - IMDB ID of the series
267+
* @param {string} season - Season number
268+
* @param {string} episode - Episode number
269+
* @returns {Promise<void>}
270+
*/
271+
const upsertSeriesProgress = async (
272+
userId: string,
273+
imdbId: string,
274+
season: string,
275+
episode: string
276+
) => {
277+
await History.findOneAndUpdate(
278+
{userId, imdbId},
279+
{$set: {type: 'series', lastSeason: Number(season), lastEpisode: Number(episode)}},
280+
{upsert: true}
281+
);
282+
};
283+
174284
/**
175285
* Configuration flag indicating if authentication should be enabled.
176286
* @type {boolean}
@@ -182,4 +292,16 @@ export const getSeriesDetail = async (id: string, season: number): Promise<Serie
182292
* app.use(passport.session());
183293
* }
184294
*/
185-
export const useAuth = appConfig.MONGO_DB_URI !== '';
295+
const useAuth = appConfig.MONGO_DB_URI !== '';
296+
297+
export {
298+
buildCanonical,
299+
buildSources,
300+
fetchAndUpdatePosters,
301+
fetchOmdbData,
302+
getResumeRedirect,
303+
getSeriesDetail,
304+
upsertMovieWatched,
305+
upsertSeriesProgress,
306+
useAuth,
307+
};

views/partials/footer.ejs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,21 @@
11
</body>
2+
<script>
3+
(function() {
4+
const btn = document.getElementById('server-toggle');
5+
const player = document.getElementById('player');
6+
if (!btn || !player) return;
7+
btn.addEventListener('click', function(e) {
8+
e.preventDefault();
9+
if (this.dataset.current === '1') {
10+
player.src = this.dataset.server2;
11+
this.dataset.current = '2';
12+
this.innerHTML = '<i class="bi bi-hdd-stack"></i> Server 1';
13+
} else {
14+
player.src = this.dataset.server1;
15+
this.dataset.current = '1';
16+
this.innerHTML = '<i class="bi bi-hdd-stack"></i> Server 2';
17+
}
18+
});
19+
})();
20+
</script>
221
</html>

0 commit comments

Comments
 (0)