Browse Source

Verify IMDb movie IDs

master
David Ludwig 4 years ago
parent
commit
dfe35be373
6 changed files with 159 additions and 18 deletions
  1. +75
    -1
      src/server/services/MovieSearch.ts
  2. +1
    -2
      src/server/services/WebServer/index.ts
  3. +22
    -0
      src/server/services/WebServer/requests/RequestMovieRequest.ts
  4. +18
    -3
      src/server/services/WebServer/routes/RouteRegisterFactory.ts
  5. +38
    -12
      src/server/services/WebServer/routes/api.ts
  6. +5
    -0
      src/server/util.ts

+ 75
- 1
src/server/services/MovieSearch.ts View File

@ -3,9 +3,12 @@ import TheMovieDb from "@lib/tmdb";
import { env, secret } from "@server/util";
import { readFile } from "fs/promises";
import TVDB from "tvdb-v4";
import { request, Agent } from "https";
import Service from "./Service";
import TvDb from "./TvDb";
const CACHE_CLEAR_INTERVAL = 1000*10; // 60 seconds
export default class MovieSearch extends Service
{
protected tmdb!: TheMovieDb;
@ -15,8 +18,23 @@ export default class MovieSearch extends Service
*/
protected tvdb!: TvDb;
/**
* Hold a cache of recently fetch movie IMDB ids to speed up request times
*/
protected imdbCache: { [imdbId: string]: number };
/**
* Hold the clear cache interval reference
*/
private __clearCacheInterval: NodeJS.Timeout | null;
/**
* Create a new Movie Search service instance
*/
public constructor(app: Application) {
super("Movie Search", app);
this.imdbCache = {};
this.__clearCacheInterval = null;
}
/**
@ -41,13 +59,69 @@ export default class MovieSearch extends Service
// no-op
}
/**
* Store an IMDb ID in cache
*/
protected cacheImdbId(id: string) {
this.imdbCache[id] = Date.now() + CACHE_CLEAR_INTERVAL;
if (this.__clearCacheInterval === null) {
this.__clearCacheInterval = setInterval(() => this.cleanImdbCache(), CACHE_CLEAR_INTERVAL);
}
}
/**
* Clean the IMDb cache
*/
protected cleanImdbCache() {
let now = Date.now();
let remaining = 0;
for (let key in this.imdbCache) {
if (now > this.imdbCache[key]) {
delete this.imdbCache[key];
} else {
remaining++;
}
}
if (remaining == 0) {
clearInterval(<NodeJS.Timeout>this.__clearCacheInterval);
this.__clearCacheInterval = null;
}
}
// Interface -----------------------------------------------------------------------------------
/**
* Verify the IMDb ID exists
*/
public verifyImdbId(id: string) {
return new Promise<boolean>((resolve, reject) => {
// If the ID is cached, no need to fetch it
if (id in this.imdbCache) {
resolve(true);
}
// Verify the movie exists on IMDb by checking for a 404
let req = request({ method: "HEAD", host: "www.imdb.com", path: `/title/${id}/` }, (response) => {
response.resume();
if (response.statusCode == undefined) {
reject();
return;
}
resolve(response.statusCode === 200);
response.destroy();
});
req.end();
});
}
/**
* Get the details of a movie
*/
public async details(id: number) {
return await this.tmdb.movie(id);
let movie = await this.tmdb.movie(id);
if (movie.imdb_id != null) {
this.cacheImdbId(movie.imdb_id);
}
return movie;
}
/**


+ 1
- 2
src/server/services/WebServer/index.ts View File

@ -85,7 +85,7 @@ export default class WebServer extends Service
*/
protected registerRoutes() {
this.registerSpaRoutes();
let factory = new RouteRegisterFactory(this.fastify, this.app);
let factory = new RouteRegisterFactory(this, this.fastify, this.app);
for (let group in routes) {
<RouteFactory>(<any>routes)[group](factory, this.app);
}
@ -98,7 +98,6 @@ export default class WebServer extends Service
/**
* If the app is in production mode, serve static assets.
* If the app is in development mode, forward 404's to Vite.
* NOTE: Static assets may be moved to nginx later... not sure yet
*/
if (process.env["NODE_ENV"] == "production") {
this.fastify.register(fastifyStatic, {


+ 22
- 0
src/server/services/WebServer/requests/RequestMovieRequest.ts View File

@ -0,0 +1,22 @@
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import Request from "./Request";
export default class MovieSearchRequest extends Request
{
public validate(request: FastifyRequest) {
return validate.async(request.params, {
imdb_id: {
presence: {
allowEmpty: false,
message: "IMDb ID is required"
},
format: {
pattern: "tt[0-9]+",
message: "Invalid IMDb ID"
}
}
},<any>{ fullMessages: false });
}
}

+ 18
- 3
src/server/services/WebServer/routes/RouteRegisterFactory.ts View File

@ -3,6 +3,7 @@ import Application from "@server/Application";
import { handleMiddleware, MiddlewareMethod } from "../middleware";
import fastifyHttpProxy from "fastify-http-proxy";
import { auth } from "../middleware/auth";
import WebServer from "..";
export type RouteFactory = ((factory: RouteRegisterFactory) => void)
| ((factory: RouteRegisterFactory, app: Application) => void);
@ -19,6 +20,11 @@ export default class RouteRegisterFactory
*/
protected readonly fastify: FastifyInstance;
/**
* The webserver instance
*/
protected readonly webserver: WebServer;
/**
* The list of middleware
*/
@ -32,9 +38,10 @@ export default class RouteRegisterFactory
/**
* Create a new route factory
*/
public constructor(fastify: FastifyInstance, app: Application) {
public constructor(webserver: WebServer, fastify: FastifyInstance, app: Application) {
this.app = app;
this.fastify = fastify;
this.webserver = webserver;
}
/**
@ -63,9 +70,9 @@ export default class RouteRegisterFactory
public get(path: string, handler: RouteHandlerMethod): void;
public get(path: string, middleware: MiddlewareMethod[], handler: RouteHandlerMethod): void;
public get(path: string, middleware: RouteHandlerMethod|MiddlewareMethod[], handler?: RouteHandlerMethod) {
this.log(`Registering GET: ${this.pathPrefix}${path}`);
handler = (handler ?? <RouteHandlerMethod>middleware);
middleware = (middleware instanceof Array) ? this.middleware.concat(middleware) : this.middleware;
console.log("Registering route:", `${this.pathPrefix}${path}`);
this.fastify.get(`${this.pathPrefix}${path}`, handleMiddleware(middleware, handler));
}
@ -84,10 +91,18 @@ export default class RouteRegisterFactory
* Register a proxy route
*/
public proxy(path: string, upstream: string, middleware?: MiddlewareMethod[]) {
this.log(`Registering proxy: ${this.pathPrefix}${path} -> ${upstream}`);
this.fastify.register(fastifyHttpProxy, {
prefix: path,
prefix: `${this.pathPrefix}${path}`,
beforeHandler: middleware ? handleMiddleware(middleware) : undefined,
upstream
});
}
/**
* Log under the WebServer service
*/
public log(...args: any[]) {
this.webserver.log(...args);
}
}

+ 38
- 12
src/server/services/WebServer/routes/api.ts View File

@ -1,27 +1,53 @@
import Application from "@server/Application";
import MovieSearch from "@server/services/MovieSearch";
import RequestMovieRequest from "../requests/RequestMovieRequest";
import { auth } from "../middleware/auth";
import RouteRegisterFactory from "./RouteRegisterFactory";
import handle from "../requests";
/**
* Register API routes
*/
export default function register(factory: RouteRegisterFactory, app: Application) {
factory.proxy("/api/tvdb/artwork", "https://artworks.thetvdb.com/");
factory.proxy("/api/tmdb/image", "https://image.tmdb.org/t/p/");
factory.prefix("/api", [auth], (factory, app) => {
/**
* Service proxies
*/
factory.proxy("/tvdb/artwork", "https://artworks.thetvdb.com/");
factory.proxy("/tmdb/image", "https://image.tmdb.org/t/p/");
factory.get("/api/movie/search", [auth], async (request, reply) => {
let query = <string>(<any>request.query)["query"];
let year = parseInt((<any>request.query)["year"]) || undefined;
let results = await app.service<MovieSearch>("Movie Search").search(query, year);
reply.send({ status: "success", data: results });
});
/**
* Search for a movie
*/
factory.get("/movie/search", async (request, reply) => {
let query = <string>(<any>request.query)["query"];
let year = parseInt((<any>request.query)["year"]) || undefined;
let results = await app.service<MovieSearch>("Movie Search").search(query, year);
reply.send({ status: "success", data: results });
});
/**
* Lookup a movie's details
*/
factory.get("/movie/details/:id", async (request, reply) => {
let id = parseInt((<any>request.params)["id"]);
let results = await app.service<MovieSearch>("Movie Search").details(id);
reply.send({ status: "success", data: results});
});
factory.get("/api/movie/details/:id", [auth], async (request, reply) => {
let id = parseInt((<any>request.params)["id"]);
let results = await app.service<MovieSearch>("Movie Search").details(id);
reply.send({ status: "success", data: results});
/**
* Request a movie to download
*/
factory.get("/api/movie/request/:imdb_id", handle([RequestMovieRequest], async (request, reply) => {
let imdbId = (<any>request.params)["imdb_id"];
if (!await app.service<MovieSearch>("Movie Search").verifyImdbId(imdbId)) {
reply.status(404);
reply.send({ satus: "Not found" });
return;
}
return reply.send({ status: "success" });
}));
});
}

+ 5
- 0
src/server/util.ts View File

@ -23,3 +23,8 @@ export async function secret(path: string) {
export function secretSync(path: string) {
return readFileSync(path).toString().trim();
}
export type Logger = ReturnType<typeof createLogger>;
export function createLogger(name: string) {
return (...args: any[]) => console.log(`[${name}]:`, ...args);
}

Loading…
Cancel
Save