Skip to content

Commit 094170d

Browse files
Merge pull request #46 from justinhartman/develop
Merge home caching and robots from develop into main
2 parents 6e5c66e + dc4a42b commit 094170d

File tree

7 files changed

+262
-95
lines changed

7 files changed

+262
-95
lines changed

controllers/appController.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import appController from './appController';
22
import http from '../helpers/httpClient';
33
import { fetchOmdbData, fetchAndUpdatePosters, getSeriesDetail } from '../helpers/appHelper';
44
import History from '../models/History';
5+
import { getLatest, setLatest, invalidateLatest } from '../helpers/cache';
56

67
jest.mock('../helpers/httpClient', () => ({
78
__esModule: true,
@@ -13,6 +14,12 @@ jest.mock('../helpers/appHelper', () => ({
1314
getSeriesDetail: jest.fn(),
1415
}));
1516

17+
jest.mock('../helpers/cache', () => ({
18+
getLatest: jest.fn(),
19+
setLatest: jest.fn(),
20+
invalidateLatest: jest.fn(),
21+
}));
22+
1623
jest.mock('../models/History', () => ({
1724
findOne: jest.fn(),
1825
findOneAndUpdate: jest.fn(),
@@ -41,6 +48,7 @@ describe('controllers/appController', () => {
4148
.mockResolvedValueOnce({ data: { result: [{ imdb_id: '1' }] } })
4249
.mockResolvedValueOnce({ data: { result: [{ imdb_id: '2' }] } });
4350
(fetchAndUpdatePosters as jest.Mock).mockResolvedValue(undefined);
51+
(getLatest as jest.Mock).mockReturnValue(undefined);
4452

4553
const req: any = { query: {}, user: { id: 1 } };
4654
const res: any = { locals: { APP_URL: 'http://app', CARD_TYPE: 'card' }, render: jest.fn() };
@@ -49,6 +57,7 @@ describe('controllers/appController', () => {
4957

5058
expect(http.get).toHaveBeenCalledTimes(2);
5159
expect(fetchAndUpdatePosters).toHaveBeenCalledTimes(2);
60+
expect(setLatest).toHaveBeenCalledWith({ movies: [{ imdb_id: '1' }], series: [{ imdb_id: '2' }] });
5261
expect(res.render).toHaveBeenCalledWith('index', expect.objectContaining({
5362
newMovies: [{ imdb_id: '1' }],
5463
newSeries: [{ imdb_id: '2' }],
@@ -62,6 +71,7 @@ describe('controllers/appController', () => {
6271
.mockResolvedValueOnce({ data: {} })
6372
.mockResolvedValueOnce({ data: {} });
6473
(fetchAndUpdatePosters as jest.Mock).mockResolvedValue(undefined);
74+
(getLatest as jest.Mock).mockReturnValue(undefined);
6575

6676
const req: any = { query: {}, user: {} };
6777
const res: any = {
@@ -77,6 +87,34 @@ describe('controllers/appController', () => {
7787
}));
7888
});
7989

90+
test('getHome returns cached results when available', async () => {
91+
(getLatest as jest.Mock).mockReturnValue({
92+
movies: [{ imdb_id: 'm1' }],
93+
series: [{ imdb_id: 's1' }],
94+
});
95+
const req: any = { query: {}, user: {} };
96+
const res: any = { locals: { APP_URL: 'http://app', CARD_TYPE: 'card' }, render: jest.fn() };
97+
98+
await appController.getHome(req, res, jest.fn());
99+
100+
expect(http.get).not.toHaveBeenCalled();
101+
expect(fetchAndUpdatePosters).not.toHaveBeenCalled();
102+
expect(res.render).toHaveBeenCalledWith('index', expect.objectContaining({
103+
newMovies: [{ imdb_id: 'm1' }],
104+
newSeries: [{ imdb_id: 's1' }],
105+
}));
106+
});
107+
108+
test('clearCache invalidates cache', async () => {
109+
const req: any = {};
110+
const res: any = { json: jest.fn() };
111+
112+
await appController.clearCache(req, res, jest.fn());
113+
114+
expect(invalidateLatest).toHaveBeenCalled();
115+
expect(res.json).toHaveBeenCalledWith({ cleared: true });
116+
});
117+
80118
test('getView renders series view', async () => {
81119
(fetchOmdbData as jest.Mock).mockResolvedValue({});
82120
const req: any = { params: { q: '', id: 'tt', type: 'series', season: '1', episode: '2' }, user: { id: 'u1' } };

controllers/appController.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { fetchOmdbData, fetchAndUpdatePosters, getSeriesDetail } from '../helper
1111
import http from '../helpers/httpClient';
1212
import History from '../models/History';
1313
import type { AuthRequest } from '../types/interfaces';
14+
import { getLatest, setLatest, invalidateLatest } from '../helpers/cache';
1415

1516
/**
1617
* @namespace appController
@@ -58,19 +59,33 @@ const appController = {
5859
const query = (req.query.q as string) || '';
5960
const type = (req.query.type as string) || 'movie';
6061
const canonical = res.locals.APP_URL;
62+
const cached = getLatest();
63+
if (cached) {
64+
return res.render('index', {
65+
newMovies: cached.movies,
66+
newSeries: cached.series,
67+
query,
68+
type,
69+
canonical,
70+
card: res.locals.CARD_TYPE,
71+
user: req.user,
72+
});
73+
}
74+
75+
const [axiosMovieResponse, axiosSeriesResponse] = await Promise.all([
76+
http.get(`https://${appConfig.VIDSRC_DOMAIN}/movies/latest/page-1.json`),
77+
http.get(`https://${appConfig.VIDSRC_DOMAIN}/tvshows/latest/page-1.json`),
78+
]);
6179

62-
const axiosMovieResponse = await http.get(
63-
`https://${appConfig.VIDSRC_DOMAIN}/movies/latest/page-1.json`
64-
);
6580
let newMovies = axiosMovieResponse.data.result || [];
66-
await fetchAndUpdatePosters(newMovies);
81+
let newSeries = axiosSeriesResponse.data.result || [];
6782

68-
const axiosSeriesResponse = await http.get(
69-
`https://${appConfig.VIDSRC_DOMAIN}/tvshows/latest/page-1.json`
70-
);
83+
await Promise.all([
84+
fetchAndUpdatePosters(newMovies),
85+
fetchAndUpdatePosters(newSeries),
86+
]);
7187

72-
let newSeries = axiosSeriesResponse.data.result || [];
73-
await fetchAndUpdatePosters(newSeries);
88+
setLatest({ movies: newMovies, series: newSeries });
7489

7590
res.render('index', {
7691
newMovies,
@@ -83,6 +98,15 @@ const appController = {
8398
});
8499
}),
85100

101+
/**
102+
* Invalidates the cached latest movies and series.
103+
* Useful for deployments or scheduled cache refreshes.
104+
*/
105+
clearCache: asyncHandler(async (_req: AuthRequest, res: Response) => {
106+
invalidateLatest();
107+
res.json({ cleared: true });
108+
}),
109+
86110
/**
87111
* Handles the "getView" route for rendering a view page based on the provided query parameters.
88112
*

helpers/cache.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getLatest, setLatest, invalidateLatest } from './cache';
2+
3+
describe('helpers/cache', () => {
4+
afterEach(() => invalidateLatest());
5+
6+
test('stores and retrieves latest content', () => {
7+
const data = { movies: [1], series: [2] } as any;
8+
setLatest(data);
9+
expect(getLatest()).toEqual(data);
10+
});
11+
12+
test('invalidateLatest clears cache', () => {
13+
setLatest({ movies: [1], series: [2] } as any);
14+
invalidateLatest();
15+
expect(getLatest()).toBeUndefined();
16+
});
17+
});

helpers/cache.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @module helpers/cache
3+
* @description In-memory caching utilities for storing latest movies and series.
4+
*/
5+
6+
import NodeCache from 'node-cache';
7+
8+
/**
9+
* TTL for cache entries in seconds (24 hours).
10+
*/
11+
const TTL_SECONDS = 60 * 60 * 24;
12+
13+
/**
14+
* Internal NodeCache instance.
15+
*/
16+
const cache = new NodeCache({ stdTTL: TTL_SECONDS });
17+
18+
const HOME_KEY = 'latest_home';
19+
20+
export interface LatestContent {
21+
movies: any[];
22+
series: any[];
23+
}
24+
25+
/**
26+
* Retrieves latest movies and series from cache.
27+
* @returns {LatestContent | undefined} Cached content if available.
28+
*/
29+
export const getLatest = (): LatestContent | undefined => {
30+
return cache.get<LatestContent>(HOME_KEY);
31+
};
32+
33+
/**
34+
* Stores latest movies and series in cache.
35+
* @param data Latest content to cache.
36+
*/
37+
export const setLatest = (data: LatestContent): void => {
38+
cache.set(HOME_KEY, data);
39+
};
40+
41+
/**
42+
* Manually clears the cached latest content.
43+
*/
44+
export const invalidateLatest = (): void => {
45+
cache.del(HOME_KEY);
46+
};
47+
48+
export default {
49+
getLatest,
50+
setLatest,
51+
invalidateLatest,
52+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"express-session": "^1.18.2",
4646
"mongodb": "^6.18.0",
4747
"mongoose": "^8.17.1",
48+
"node-cache": "^5.1.2",
4849
"passport": "^0.7.0",
4950
"passport-local": "^1.0.0"
5051
},

public/robots.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# NOTICE: The collection of content and other data on
2+
# this site through automated means, including any
3+
# device, tool, or process designed to data mine or
4+
# scrape content, is prohibited except (1) as provided by
5+
# the below Content Usage directives or (2) with express
6+
# written permission from this website's owner.
7+
8+
# To request permission to license our intellectual
9+
# property and/or other materials, please contact us at
10+
# hello@binger.uk
11+
12+
User-Agent: AhrefsBot
13+
Disallow: /
14+
15+
User-Agent: AhrefsSiteAudit
16+
Disallow: /
17+
18+
User-agent: CensysInspect
19+
Disallow: /
20+
21+
User-Agent: *
22+
Content-Usage: tdm=n, search=y, inference=y
23+
Allow: /

0 commit comments

Comments
 (0)