diff --git a/services/request/nodemon.json b/services/request/nodemon.json index 0cf4da5..773c421 100644 --- a/services/request/nodemon.json +++ b/services/request/nodemon.json @@ -1,8 +1,8 @@ { - "watch": ["src/common", "src/server"], + "watch": ["src"], "ext": "ts,json", "ignore": ["src/**/*.spec.ts"], - "exec": "node --inspect=0.0.0.0:9229 -r tsconfig-paths/register -r ts-node/register src/server/index.ts", + "exec": "node --inspect=0.0.0.0:9229 -r tsconfig-paths/register -r ts-node/register src/index.ts", "events": { "start": "clear" } diff --git a/services/request/package.json b/services/request/package.json index cf9386c..54c0e3c 100644 --- a/services/request/package.json +++ b/services/request/package.json @@ -4,7 +4,7 @@ "keywords": [], "author": "David Ludwig", "license": "ISC", - "main": "./dist/server/index.js", + "main": "./dist/index.js", "scripts": { "clean": "rimraf ./dist", "build": "ttsc", @@ -12,6 +12,9 @@ "start:dev": "nodemon" }, "dependencies": { + "@autoplex-api/plex": "^0.0.0", + "@autoplex-api/request": "^0.0.0", + "@autoplex-api/search": "^0.0.0", "@autoplex-api/seeker": "^0.0.0", "@autoplex/database": "^0.0.0", "@autoplex/ipc": "^0.0.0", diff --git a/services/request/src/server/Application.ts b/services/request/src/Application.ts similarity index 100% rename from services/request/src/server/Application.ts rename to services/request/src/Application.ts diff --git a/services/request/src/server/common.ts b/services/request/src/common.ts similarity index 100% rename from services/request/src/server/common.ts rename to services/request/src/common.ts diff --git a/services/request/src/common/api_schema.ts b/services/request/src/common/api_schema.ts deleted file mode 100644 index 3598772..0000000 --- a/services/request/src/common/api_schema.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IMovieDetails, IPaginatedResponse } from "@lib/tmdb/schema"; - -/** - * Basic user information schema - */ -export interface IUser { - id : number, - name : string, - isAdmin: boolean -} - -/** - * The JWT auth token structure - */ -export interface ITokenSchema extends IUser { - iat : number, - exp : number -} - -/** - * The general API response structure - */ -export interface IApiResponse { - status: string -} - -/** - * A generic data response from the API - */ -export interface IApiDataResponse extends IApiResponse { - data: T -} - -export interface IApiPaginatedResponse { - page : number, - results : T[], - totalPages : number, - totalResults: number -}; - -/** - * A movie listing returned from the API - */ -export interface IApiMovie { - plexLink : string | null, - posterPath : string | null, - releaseDate: string | null, - ticketId : number | null, - title : string, - tmdbId : number -} - -/** - * Movie details returned from the API - */ -export interface IApiMovieDetails extends IApiMovie { - backdropPath: string | null, - imdbId : string | null, - overview : string | null, - runtime : number | null, - requestedBy : IUser | null -} - -/** - * Movie detail data - */ -// export interface IApiMovieDetails extends Pick -// { -// is_requested: boolean -// } - -/** - * - */ -// export type IApiMovieDetailsResponse = IApiDataResponse; - -// export interface IApiMovieTicket { -// id : number, -// tmdbId : number, -// imdbId : number, -// title : string, -// overview : string, -// posterPath : string, -// backdropPath: string, -// runtime : number, -// releaseDate : string, -// } - -// export type IApiMovieTicketResponse = IApiDataResponse; diff --git a/services/request/src/common/validation.ts b/services/request/src/common/validation.ts deleted file mode 100644 index 19ea394..0000000 --- a/services/request/src/common/validation.ts +++ /dev/null @@ -1,90 +0,0 @@ -export const constraints = { - api: { - movie: { - search: { - query: { - presence: { - allowEmpty: false, - message: "The query cannot be blank" - } - }, - year: { - numericality: { - onlyInteger: true, - greaterThan: 0, - notGreaterThan: "Invalid year", - notValid: "Invalid year", - notInteger: "Invalid year" - } - } - } - } - }, - login: { - email: { - presence: { - allowEmpty: false, - message: "An email address is required" - } - }, - password: { - presence: { - allowEmpty: false, - message: "A password is required" - } - } - }, - register: { - token: { - presence: { - message: "A valid token is required to register" - }, - token: { - message: "A valid token is required to register" - } - }, - name: { - presence: { - allowEmpty: false, - message: "Your name is required" - }, - length: { - maximum: 50, - tooLong: "Your name cannot exceed 50 characters" - } - }, - email: { - presence: { - allowEmpty: false, - message: "Your email is required" - }, - length: { - maximum: 255, - tooLong: "An email address cannot exceed 255 characters" - }, - email: { - message: "A valid email address is required" - } - }, - password: { - presence: { - allowEmpty: false, - message: "A password is required" - }, - length: { - minimum: 8, - tooShort: "Password should be at least 8 characters" - } - }, - retypePassword: { - presence: { - allowEmpty: false, - message: "Re-type your password to confirm it" - }, - equality: { - attribute: "password", - message: "Passwords must match" - } - } - } -}; diff --git a/services/request/src/server/index.ts b/services/request/src/index.ts similarity index 100% rename from services/request/src/server/index.ts rename to services/request/src/index.ts diff --git a/services/request/src/lib/plex/index.ts b/services/request/src/lib/plex/index.ts deleted file mode 100644 index 97eefb9..0000000 --- a/services/request/src/lib/plex/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { parseStringPromise } from "xml2js"; -import { readFileSync } from "fs"; -import ApiRequestManager from "./request"; -import { ILibraryContents } from "./schema"; - -/** - * Map a media ID such as an IMDb ID to a Plex movie key - */ -interface IMediaMap { - [id: string]: number -} - -/** - * An interface to the a Plex server - */ -export default class Plex -{ - /** - * The API request manager instance - */ - protected requestManager: ApiRequestManager; - - /** - * Create a new instance of the Plex interface - */ - public constructor(plexToken: string) { - this.requestManager = new ApiRequestManager(plexToken); - } - - /** - * Fetch the IMDb ID set from the Plex movies library - */ - public async movies(key: string|number) { - let response = await this.requestManager.get(`/library/sections/${key}/all`); - let library = await parseStringPromise(response); - let movies: IMediaMap = {}; - for (let video of library.MediaContainer.Video) { - let imdbMatch = video.$.guid.match(/(?<=imdb\:\/\/)tt\d+/i); - if (imdbMatch === null) { - continue; - } - let imdbId = imdbMatch[0]; - let plexKey = parseInt(video.$.ratingKey); - movies[imdbId] = plexKey; - } - return movies; - } -} diff --git a/services/request/src/lib/plex/request.ts b/services/request/src/lib/plex/request.ts deleted file mode 100644 index 6656cf0..0000000 --- a/services/request/src/lib/plex/request.ts +++ /dev/null @@ -1,112 +0,0 @@ -import https, { RequestOptions } from "https"; - -/** - * The URL to the Plex server - */ -const API_URL = process.env["PLEX_URL"]; - -/** - * A status error is used to indicate responses with non-200 status codes - */ -export class StatusError extends Error -{ - /** - * The resulting body of a response - */ - public readonly response: T; - - /** - * The resulting status code of a response - */ - public readonly statusCode?: number; - - /** - * Create a new error indicating non-200 status - */ - public constructor(response: T, statusCode: number) { - super(); - Object.setPrototypeOf(this, StatusError.prototype); - this.response = response; - this.statusCode = statusCode; - } -} - -/** - * A request manager with atomic/persistent request options - */ -export default class ApiRequestManager -{ - /** - * The authorization token - */ - private __plexToken: string; - - /** - * Store additional request options - */ - protected options: RequestOptions; - - /** - * Create a new API request manager - * - * @param options Additional request options - */ - public constructor(plexToken: string, options: RequestOptions = {}) { - this.__plexToken = plexToken; - this.options = options; - } - - /** - * Perform a generic HTTPS request - * - * @param method The HTTP method - * @param url The URL to request - * @param apiKey An optional bearer token - * @param params Optional parameters - * @param body Optional body - */ - protected request(method: string, url: string, params?: any) - { - return new Promise((resolve, reject) => { - // Create request options - let options = Object.assign({ method, headers: {} }, this.options); - - // Add search parameters if necessary - let requestUrl = new URL(url); - requestUrl.searchParams.set("X-Plex-Token", this.__plexToken); - if (params) { - Object.keys(params).forEach((key) => { - if (params[key] !== undefined) { - requestUrl.searchParams.set(key, params[key]); - } - }); - } - - // Create the request - let request = https.request(requestUrl, options, (res) => { - let rawData: string = ""; - res.setEncoding("utf8"); - res.on("data", chunk => {rawData += chunk}); - res.on("error", reject); - res.on("end", async () => { - rawData; - if (res.statusCode == 200) { - resolve(rawData) - } else { - reject(new StatusError(rawData, res.statusCode)); - } - }); - }) - .on("error", reject) - .on("timeout", () => reject("timeout")); - request.end(); - }); - } - - /** - * Perform a generic GET request - */ - public async get(path: string, params?: any) { - return await this.request("GET", `${API_URL}${path}`, params); - } -} diff --git a/services/request/src/lib/plex/schema.ts b/services/request/src/lib/plex/schema.ts deleted file mode 100644 index 26a41ae..0000000 --- a/services/request/src/lib/plex/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface IVideo { - $: { - ratingKey: string, - guid: string - } -} - -export interface ILibraryContents { - MediaContainer: { - Video: IVideo[] - } -} diff --git a/services/request/src/lib/tmdb/index.ts b/services/request/src/lib/tmdb/index.ts deleted file mode 100644 index 8a2759f..0000000 --- a/services/request/src/lib/tmdb/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import ApiRequestManager from "./request" -import * as Schema from "./schema"; - -export enum ExternalSource { - Facebook = "facebook_id", - Freebase = "freebase_id", - FreebaseM = "freebase_mid", - Imdb = "imdb_id", - Instagram = "instagram_id", - Tvdb = "tvdb_id", - TvRage = "tvrage_id", - Twitter = "twitter_id" -} - -export default class TheMovieDb -{ - protected requestManager!: ApiRequestManager; - - public constructor(apiKey: string) { - this.requestManager = new ApiRequestManager(apiKey); - } - - public async configuration() { - return await this.requestManager.get("/configuration"); - } - - public async searchMovie(query: string, year?: number, page?: number) { - return await this.requestManager.get>("/search/movie", { query, year }); - } - - public async movie(id: number) { - return await this.requestManager.get(`/movie/${id}`); - } - - public async findMovie(id: string, externalSource: ExternalSource) { - return await this.requestManager.get(`/find/${id}`, { external_source: externalSource }); - } -} diff --git a/services/request/src/lib/tmdb/request.ts b/services/request/src/lib/tmdb/request.ts deleted file mode 100644 index cbabd1a..0000000 --- a/services/request/src/lib/tmdb/request.ts +++ /dev/null @@ -1,126 +0,0 @@ -import https, { RequestOptions } from "https"; - -/** - * The API URL - */ -const API_URL = "https://api.themoviedb.org/3"; - -/** - * A status error is used to indicate responses with non-200 status codes - */ -export class StatusError extends Error -{ - /** - * The resulting body of a response - */ - public readonly response: T; - - /** - * The resulting status code of a response - */ - public readonly statusCode?: number; - - /** - * Create a new error indicating non-200 status - */ - public constructor(response: T, statusCode: number) { - super(); - Object.setPrototypeOf(this, StatusError.prototype); - this.response = response; - this.statusCode = statusCode; - } -} - -/** - * A request manager with atomic/persistent request options - */ -export default class ApiRequestManager -{ - private __api_key: string; - - /** - * Store additional request options - */ - protected options: RequestOptions; - - /** - * Create a new API request manager - * - * @param options Additional request options - */ - public constructor(apiKey: string, options: RequestOptions = {}) { - this.__api_key = apiKey; - this.options = options; - } - - /** - * Perform a generic HTTPS request - * - * @param method The HTTP method - * @param url The URL to request - * @param apiKey An optional bearer token - * @param params Optional parameters - * @param body Optional body - */ - protected request(method: string, url: string, params?: any, body?: string) - { - return new Promise((resolve, reject) => { - // Create request options - let options = Object.assign({ method, headers: {} }, this.options); - if (body) { - options.headers["Content-Type"] = "application/json"; - options.headers["Content-Length"] = body.length; - } - - // Add search parameters if necessary - let requestUrl = new URL(url); - requestUrl.searchParams.set("api_key", this.__api_key); - if (params) { - Object.keys(params).forEach((key) => { - if (params[key] !== undefined) { - requestUrl.searchParams.set(key, params[key]); - } - }); - } - - // Create the request - let request = https.request(requestUrl, options, (res) => { - let rawData: string = ""; - res.setEncoding("utf8"); - res.on("data", chunk => {rawData += chunk}); - res.on("error", reject); - res.on("end", () => { - let response: T = JSON.parse(rawData); - if (res.statusCode == 200) { - resolve(response) - } else { - reject(new StatusError(response, res.statusCode)); - } - }); - }) - .on("error", reject) - .on("timeout", () => reject("timeout")); - if (body) { - request.write(body); - } - request.end(); - }); - } - - /** - * Perform a generic GET request - */ - public async get(path: string, params?: any) { - return await this.request("GET", `${API_URL}${path}`, params); - } - - /** - * Perform a generic POST request - */ - public async post(path: string, params?: any, body?: any) { - if (body !== undefined) { - body = JSON.stringify(body); - } - return await this.request("POST", `${API_URL}${path}`, params, body); - } -} diff --git a/services/request/src/lib/tmdb/schema.ts b/services/request/src/lib/tmdb/schema.ts deleted file mode 100644 index 25b34c6..0000000 --- a/services/request/src/lib/tmdb/schema.ts +++ /dev/null @@ -1,89 +0,0 @@ -export enum Status { - Rumored = "Rumored", - Planned = "Planned", - InProduction = "InProduction", - PostProduction = "PostProduction", - Released = "Released", - Canceled = "Canceled" -} - -export interface IGenre { - id : number, - name: string -} - -export interface ILanguage { - iso_639_1: string, - name: string -} - -export interface IFindResult { - movie_results : IMovieSearchResult[], - person_results : unknown, - tv_results : unknown, - tv_episode_results: unknown, - tv_season_results : unknown -} - -export interface IMovieSearchResult { - adult : boolean, - backdrop_path : string | null, - genre_ids : number[], - id : number, - original_string: string, - original_title : string, - overview : string, - popularity : number, - poster_path : string | null, - release_date : string, - title : string, - video : boolean, - vote_average : number - vote_count : number, -} - -export interface IMovieDetails { - adult : boolean, - backdrop_path : string | null, - belongs_to_collection: any, - budget : number, - genres : IGenre[] - imdb_id : string | null, - original_language : string, - original_title : string, - overview : string | null, - popularity : number, - poster_path : string | null, - production_companies : IProductionCompany[], - production_countries : IProductionCountry[], - release_date : string, - revenue : number, - runtime : number | null, - spoken_languages : ILanguage[], - status : Status, - tagline : string | null, - title : string, - video : boolean, - vote_average : number, - vote_count : number -} - -export interface IProductionCompany { - name : string, - id : number, - logo_path : string | null, - origin_country: string -} - -export interface IProductionCountry { - release_date: string, - revenue : number, - runtime : number | null -} - -export interface IPaginatedResponse { - page : number, - results : T[], - total_results: number, - total_pages : number -} diff --git a/services/request/src/server/services/MovieSearch.ts b/services/request/src/server/services/MovieSearch.ts deleted file mode 100644 index 2f6de6e..0000000 --- a/services/request/src/server/services/MovieSearch.ts +++ /dev/null @@ -1,176 +0,0 @@ -import TheMovieDb, { ExternalSource } from "@lib/tmdb"; -import { env, plexMediaUrl, secret } from "@autoplex/utils"; -import { request } from "https"; -import { InternalService } from "@autoplex/microservice"; -import TvDb from "./TvDb"; -import { IApiMovie, IApiMovieDetails, IApiPaginatedResponse } from "@common/api_schema"; -import { MovieTicket, PlexMovie } from "@autoplex/database"; -import { IMovieSearchResult } from "@lib/tmdb/schema"; -import Application from "@server/Application"; - -const CACHE_CLEAR_INTERVAL = 1000*60; // 60 seconds - -export default class MovieSearch extends InternalService -{ - /** - * A reference to The Movie DB API - */ - protected tmdb!: TheMovieDb; - - /** - * The instance of TVDB - */ - protected tvdb!: TvDb; - - /** - * Hold a cache of recently fetched movies to speed up request times - */ - protected movieCache: { [tmdbId: number]: { timestamp: number, movie: IApiMovieDetails } } = {}; - - /** - * Hold the clear cache interval reference - */ - private __clearCacheInterval: NodeJS.Timeout|null = null; - - /** - * The name of the service - */ - public readonly NAME = "Movie Search"; - - /** - * Start the service - */ - public start() { - this.tvdb = this.app.service("TVDB"); - } - - /** - * Boot the service - */ - public async boot() { - let apiKey = await secret(env("TMDB_KEY_FILE")); - this.tmdb = new TheMovieDb(apiKey); - } - - /** - * Store an IMDb ID in cache - */ - protected cacheMovie(tmdbId: number, movie: IApiMovieDetails) { - this.movieCache[tmdbId] = { movie, timestamp: Date.now() + CACHE_CLEAR_INTERVAL }; - if (this.__clearCacheInterval === null) { - this.__clearCacheInterval = setInterval(() => this.cleanMovieCache(), CACHE_CLEAR_INTERVAL); - } - return this.movieCache[tmdbId]; - } - - /** - * Clean the IMDb cache - */ - protected cleanMovieCache() { - let now = Date.now(); - let remaining = 0; - for (let key in this.movieCache) { - if (now > this.movieCache[key].timestamp) { - delete this.movieCache[key]; - } else { - remaining++; - } - } - if (remaining == 0) { - clearInterval(this.__clearCacheInterval); - this.__clearCacheInterval = null; - } - } - - // Interface ----------------------------------------------------------------------------------- - - /** - * Verify the IMDb ID exists - */ - public verifyImdbId(id: string) { - return new Promise((resolve, reject) => { - 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) { - if (id in this.movieCache) { - if (this.movieCache[id].movie.ticketId == null) { - let ticket = await MovieTicket.findOne({ where: { tmdbId: id, isCanceled: false }, relations: ["user"] }); - this.movieCache[id].movie.ticketId = ticket?.id ?? null; - this.movieCache[id].movie.requestedBy = ticket ? { - id : ticket.user.id, - isAdmin: ticket.user.isAdmin, - name : ticket.user.name - } : null; - } - return this.movieCache[id].movie; - } - let fetchMovieRequest = this.tmdb.movie(id); - let ticket = await MovieTicket.findOne({ where: { tmdbId: id, isCanceled: false }, relations: ["user"] }); - let movie = await fetchMovieRequest; - let plexKey = await PlexMovie.findPlexKey(id); - let result: IApiMovieDetails = { - tmdbId : id, - backdropPath: movie.backdrop_path, - imdbId : movie.imdb_id, - overview : movie.overview, - plexLink : plexKey !== null ? plexMediaUrl(plexKey) : null, - posterPath : movie.poster_path, - releaseDate : movie.release_date, - runtime : movie.runtime, - title : movie.title, - ticketId : ticket?.id ?? null, - requestedBy : (ticket ? { - id : ticket.user.id, - isAdmin: ticket.user.isAdmin, - name : ticket.user.name - } : null) - }; - return this.cacheMovie(id, result).movie; - } - - /** - * Find a movie by its IMDb ID - */ - public async findImdb(imdbId: string): Promise { - return (await this.tmdb.findMovie(imdbId, ExternalSource.Imdb)).movie_results[0] ?? null; - } - - /** - * Search for a movie - */ - public async search(query: string, year?: number) { - let movieFetchRequest = this.tmdb.searchMovie(query, year); - let activeTickets = await MovieTicket.activeTicketMap(); - let results = await movieFetchRequest; - let plexKeys = await Promise.all(results.results.map( - movie => PlexMovie.findPlexKey(movie.id) - )); - return >{ - page: results.page, - results: results.results.map((movie, index) => { - plexLink : plexKeys[index] !== null ? plexMediaUrl(plexKeys[index]) : null, - posterPath : movie.poster_path, - releaseDate: movie.release_date, - ticketId : activeTickets[movie.id] ?? null, - title : movie.title, - tmdbId : movie.id - }), - totalPages: results.total_pages, - totalResults: results.total_results - }; - } -} diff --git a/services/request/src/server/services/PlexLibrary.ts b/services/request/src/server/services/PlexLibrary.ts deleted file mode 100644 index 724dfdc..0000000 --- a/services/request/src/server/services/PlexLibrary.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Plex from "@lib/plex"; -import { PlexMovie } from "@autoplex/database"; -import { env, secret, sleep } from "@autoplex/utils"; -import MovieSearch from "./MovieSearch"; -import { InternalService } from "@autoplex/microservice"; -import Application from "@server/Application"; - -/** - * Throttle requests when updating the movie database - */ -const API_DATABASE_THROTTLE = 500; - -/** - * A service for maintaining an internal representation of the Plex library - */ -export default class PlexLibrary extends InternalService -{ - /** - * A reference to the Plex library - */ - protected plex!: Plex; - - /** - * The key for the movies library - */ - protected readonly KEY_MOVIES: string = env("PLEX_LIBRARY_MOVIES_KEY"); - - /** - * The key for the TV shows library - */ - protected readonly KEY_TV: string = env("PLEX_LIBRARY_TV_KEY"); - - /** - * Indicate if the plex library is currently being updated - */ - protected isUpdating: boolean = false; - - /** - * The service name - */ - public readonly NAME = "Plex Library"; - - /** - * Boot the Plex library service - */ - public async boot() { - let token = await secret(env("PLEX_TOKEN_FILE")); - this.plex = new Plex(token); - } - - /** - * Update the movies after boot - */ - public start() { - this.updateMovies(); - } - - /** - * Update the movie catalog - */ - public async updateMovies() { - - } -} diff --git a/services/request/src/server/services/TvDb.ts b/services/request/src/server/services/TvDb.ts deleted file mode 100644 index 2a5e9b7..0000000 --- a/services/request/src/server/services/TvDb.ts +++ /dev/null @@ -1,58 +0,0 @@ -import TVDB from "tvdb-v4"; -import { InternalService } from "@autoplex/microservice"; -import { env, secret } from "@autoplex/utils"; -import Application from "@server/Application"; - -/** - * The token refresh period in milliseconds - */ -const TOKEN_REFRESH_PERIOD = 1000*60*parseInt(env("TVDB_REFRESH_PERIOD")); - -export default class TvDb extends InternalService -{ - /** - * The active TVDB instance - */ - protected tvdb!: TVDB; - - /** - * Store the next timestamp when a token refresh is needed - */ - protected nextTokenRefreshTimestamp: number = 0; - - /** - * The service name - */ - public readonly NAME = "TVDB"; - - /** - * Boot the service - */ - public async boot() { - let apiKey = await secret(env("TVDB_KEY_FILE")); - this.tvdb = new TVDB(apiKey); - await this.refreshLogin(); - } - - /** - * Refresh the login token if necessary - */ - protected async refreshLogin() { - if (Date.now() < this.nextTokenRefreshTimestamp) { - return; - } - this.log("Refreshing login token..."); - let pin = await secret(env("TVDB_PIN_FILE")); - let timestamp = Date.now() + TOKEN_REFRESH_PERIOD; // Save the time before the request - await this.tvdb.login(pin); - this.nextTokenRefreshTimestamp = timestamp - } - - /** - * Search for a movie - */ - public async searchMovie(query: string, year?: number) { - await this.refreshLogin(); - return await this.tvdb.search(query, "movie", year); - } -} diff --git a/services/request/src/server/services/index.ts b/services/request/src/server/services/index.ts deleted file mode 100644 index fa9eef8..0000000 --- a/services/request/src/server/services/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { IpcClient } from "@autoplex-api/seeker"; -export { DatabaseService } from "@autoplex/database"; -import DiscordBot from "./DiscordBot"; -import MovieSearch from "./MovieSearch"; -import PlexLibrary from "./PlexLibrary"; -import TvDb from "./TvDb"; -import WebServer from "./WebServer/WebServer"; - -export { - // DiscordBot, - MovieSearch, - PlexLibrary, - // TorrentIpcClient, - TvDb, - WebServer -} diff --git a/services/request/src/server/services/DiscordBot.ts b/services/request/src/services/DiscordBot.ts similarity index 99% rename from services/request/src/server/services/DiscordBot.ts rename to services/request/src/services/DiscordBot.ts index ba88381..9a5f0c1 100644 --- a/services/request/src/server/services/DiscordBot.ts +++ b/services/request/src/services/DiscordBot.ts @@ -2,8 +2,8 @@ import { InternalService } from "@autoplex/microservice"; import { Client, Collection, Message, TextChannel, User as DiscordUser } from "discord.js"; import { env, formatImdbId, generateToken, secret } from "@autoplex/utils"; import { DiscordAccount, DiscordChannel, DiscordLinkRequest, MovieTicket } from "@autoplex/database"; +import Application from "../Application"; import MovieSearch from "./MovieSearch"; -import Application from "@server/Application"; /** * The required role to perfrom administrative commands on the bot @@ -392,7 +392,7 @@ export default class DiscordBot extends InternalService `Unfortunately, the movie you requested could net be located: https://imdb.com/title/${imdbId}/`); return; } - let info = await this.app.service("Movie Search").details(movie.id); + let info = await this.app.service("Movie Search").details(movie.tmdbId); // await MovieTicket.requestTmdb(account.user, info); this.sendDm(message.author, `*${info.title}* has been requested successfully! (TEMP DISABLE)`); } diff --git a/services/request/src/services/MovieSearch.ts b/services/request/src/services/MovieSearch.ts new file mode 100644 index 0000000..02c3967 --- /dev/null +++ b/services/request/src/services/MovieSearch.ts @@ -0,0 +1,86 @@ +import { MovieTicket } from "@autoplex/database"; +import { IpcClient as PlexIpc } from "@autoplex-api/plex"; +import { IMovie, IMovieDetails, IPaginatedResponse } from "@autoplex-api/request"; +import { IpcClient as SearchIpc } from "@autoplex-api/search"; +import { InternalService } from "@autoplex/microservice"; +import Application from "../Application"; + +export default class MovieSearch extends InternalService +{ + /** + * The name of the service + */ + public readonly NAME = "Movie Search"; + + /** + * The Plex IPC interface + */ + protected plex!: PlexIpc; + + /** + * A reference to The Movie DB API + */ + protected searchIpc!: SearchIpc; + + /** + * Start the service + */ + public start() { + this.plex = this.app.service("Plex"); + this.searchIpc = this.app.service("Search"); + } + + // Interface ----------------------------------------------------------------------------------- + + /** + * Get the details of a movie + */ + public async details(tmdbId: number) { + let [ movie, plexLinks, ticket ] = await Promise.all([ + this.searchIpc.movieDetails(tmdbId), + this.plex.movieLinks([tmdbId]), + MovieTicket.findOne({ + where: { tmdbId: tmdbId, isCanceled: false }, + relations: ["user"] + }) + ]); + return Object.assign(movie, { + plexLink: plexLinks[tmdbId], + ticketId : ticket?.id ?? null, + requestedBy : (ticket ? { + id : ticket.user.id, + isAdmin: ticket.user.isAdmin, + name : ticket.user.name + } : null) + }); + } + + /** + * Find a movie by its IMDb ID + */ + public async findImdb(imdbId: string) { + return await this.searchIpc.findMovieFromImdb(imdbId); + } + + /** + * Search for a movie + */ + public async search(query: string, year?: number) { + this.log("Searching for John Wick..."); + let [ movies, ticketMap ] = await Promise.all([ + this.searchIpc.searchMovie(query, year), + MovieTicket.activeTicketMap() + ]); + let plexLinks = await this.plex.movieLinks(movies.results.map(movie => movie.tmdbId)); + return >{ + page: movies.page, + results: movies.results.map(movie => { + ...movie, + plexLink: plexLinks[movie.tmdbId], + ticketId : ticketMap[movie.tmdbId] ?? null + }), + totalPages: movies.totalPages, + totalResults: movies.totalPages + }; + } +} diff --git a/services/request/src/server/services/WebServer/WebServer.ts b/services/request/src/services/WebServer/WebServer.ts similarity index 92% rename from services/request/src/server/services/WebServer/WebServer.ts rename to services/request/src/services/WebServer/WebServer.ts index 41b5d40..5668e68 100644 --- a/services/request/src/server/services/WebServer/WebServer.ts +++ b/services/request/src/services/WebServer/WebServer.ts @@ -1,8 +1,8 @@ -import Application from "@server/Application"; -import routes from "./routes"; -import "./validators"; import { WebServerService } from "@autoplex/webserver"; import { env } from "@autoplex/utils"; +import Application from "../../Application"; +import routes from "./routes"; +import "./validators"; export default class WebServer extends WebServerService { diff --git a/services/request/src/server/services/WebServer/middleware/auth.ts b/services/request/src/services/WebServer/middleware/auth.ts similarity index 88% rename from services/request/src/server/services/WebServer/middleware/auth.ts rename to services/request/src/services/WebServer/middleware/auth.ts index 58b4781..d7e2120 100644 --- a/services/request/src/server/services/WebServer/middleware/auth.ts +++ b/services/request/src/services/WebServer/middleware/auth.ts @@ -1,9 +1,8 @@ -import { IteratorNext, MiddlewareRequest } from "@autoplex/webserver"; -import { FastifyReply } from "fastify"; -import Application from "@server/Application"; -import jwt from "jsonwebtoken"; -import { ITokenSchema } from "@common/api_schema"; +import { ITokenSchema } from "@autoplex-api/request"; import { User } from "@autoplex/database"; +import { IteratorNext, MiddlewareRequest, FastifyReply } from "@autoplex/webserver"; +import jwt from "jsonwebtoken"; +import Application from "../../../Application"; /** * Attempt to authenticate a client's JWT token diff --git a/services/request/src/server/services/WebServer/middleware/index.ts b/services/request/src/services/WebServer/middleware/index.ts similarity index 100% rename from services/request/src/server/services/WebServer/middleware/index.ts rename to services/request/src/services/WebServer/middleware/index.ts diff --git a/services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts b/services/request/src/services/WebServer/requests/LinkDiscordRequest.ts similarity index 100% rename from services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts rename to services/request/src/services/WebServer/requests/LinkDiscordRequest.ts diff --git a/services/request/src/server/services/WebServer/requests/LoginRequest.ts b/services/request/src/services/WebServer/requests/LoginRequest.ts similarity index 85% rename from services/request/src/server/services/WebServer/requests/LoginRequest.ts rename to services/request/src/services/WebServer/requests/LoginRequest.ts index 0c266ea..c578977 100644 --- a/services/request/src/server/services/WebServer/requests/LoginRequest.ts +++ b/services/request/src/services/WebServer/requests/LoginRequest.ts @@ -1,7 +1,7 @@ +import { ValidationConstraints as constraints } from "@autoplex-api/request"; +import { Request } from "@autoplex/webserver"; import { FastifyRequest } from "fastify"; import validate from "validate.js"; -import { constraints } from "@common/validation"; -import { Request } from "@autoplex/webserver"; export interface ILoginFormBody { email: string, diff --git a/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts b/services/request/src/services/WebServer/requests/MovieSearchRequest.ts similarity index 83% rename from services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts rename to services/request/src/services/WebServer/requests/MovieSearchRequest.ts index b63a708..92a4f17 100644 --- a/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts +++ b/services/request/src/services/WebServer/requests/MovieSearchRequest.ts @@ -1,7 +1,7 @@ -import { constraints } from "@common/validation"; +import { ValidationConstraints as constraints } from "@autoplex-api/request"; +import { Request } from "@autoplex/webserver"; import { FastifyRequest } from "fastify"; import validate from "validate.js"; -import { Request } from "@autoplex/webserver"; export class MovieSearchRequest extends Request { diff --git a/services/request/src/server/services/WebServer/requests/RegisterRequest.ts b/services/request/src/services/WebServer/requests/RegisterRequest.ts similarity index 89% rename from services/request/src/server/services/WebServer/requests/RegisterRequest.ts rename to services/request/src/services/WebServer/requests/RegisterRequest.ts index 088d4c3..c609fbc 100644 --- a/services/request/src/server/services/WebServer/requests/RegisterRequest.ts +++ b/services/request/src/services/WebServer/requests/RegisterRequest.ts @@ -1,7 +1,7 @@ +import { ValidationConstraints as constraints } from "@autoplex-api/request"; +import { Request } from "@autoplex/webserver"; import { FastifyRequest } from "fastify"; import validate from "validate.js"; -import { constraints } from "@common/validation"; -import { Request } from "@autoplex/webserver"; export interface IRegisterFormBody { token: string, diff --git a/services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts b/services/request/src/services/WebServer/requests/RequestImdbMovieRequest.ts similarity index 100% rename from services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts rename to services/request/src/services/WebServer/requests/RequestImdbMovieRequest.ts diff --git a/services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts b/services/request/src/services/WebServer/requests/RequestMovieRequest.ts similarity index 100% rename from services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts rename to services/request/src/services/WebServer/requests/RequestMovieRequest.ts diff --git a/services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts b/services/request/src/services/WebServer/requests/RequestTmdbMovieRequest.ts similarity index 100% rename from services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts rename to services/request/src/services/WebServer/requests/RequestTmdbMovieRequest.ts diff --git a/services/request/src/server/services/WebServer/requests/index.ts b/services/request/src/services/WebServer/requests/index.ts similarity index 100% rename from services/request/src/server/services/WebServer/requests/index.ts rename to services/request/src/services/WebServer/requests/index.ts diff --git a/services/request/src/server/services/WebServer/routes/api.ts b/services/request/src/services/WebServer/routes/api.ts similarity index 98% rename from services/request/src/server/services/WebServer/routes/api.ts rename to services/request/src/services/WebServer/routes/api.ts index 0b6a783..19ffff2 100644 --- a/services/request/src/server/services/WebServer/routes/api.ts +++ b/services/request/src/services/WebServer/routes/api.ts @@ -1,8 +1,8 @@ import { IpcClient as SeekerIpc } from "@autoplex-api/seeker"; import { MovieInfo, MovieTicket } from "@autoplex/database"; import { handleRequest, RouteRegisterFactory, MiddlewareMethod } from "@autoplex/webserver"; -import Application from "@server/Application"; -import MovieSearch from "@server/services/MovieSearch"; +import Application from "../../../Application"; +import MovieSearch from "../../MovieSearch"; import { auth } from "../middleware/auth"; import { RequestTmdbMovieRequest } from "../requests"; diff --git a/services/request/src/server/services/WebServer/routes/auth.ts b/services/request/src/services/WebServer/routes/auth.ts similarity index 98% rename from services/request/src/server/services/WebServer/routes/auth.ts rename to services/request/src/services/WebServer/routes/auth.ts index a34ddfe..a5ba9fe 100644 --- a/services/request/src/server/services/WebServer/routes/auth.ts +++ b/services/request/src/services/WebServer/routes/auth.ts @@ -1,7 +1,7 @@ import { DiscordLinkRequest, RegisterToken, User } from "@autoplex/database"; import { RouteRegisterFactory, handleRequest, MiddlewareMethod } from "@autoplex/webserver"; import jwt from "jsonwebtoken" -import Application from "@server/Application"; +import Application from "../../../Application"; import { LoginRequest, ILoginFormBody } from "../requests"; import { RegisterRequest, IRegisterFormBody } from "../requests"; import { LinkDiscordRequest } from "../requests"; diff --git a/services/request/src/server/services/WebServer/routes/index.ts b/services/request/src/services/WebServer/routes/index.ts similarity index 100% rename from services/request/src/server/services/WebServer/routes/index.ts rename to services/request/src/services/WebServer/routes/index.ts diff --git a/services/request/src/server/services/WebServer/routes/web.ts b/services/request/src/services/WebServer/routes/web.ts similarity index 86% rename from services/request/src/server/services/WebServer/routes/web.ts rename to services/request/src/services/WebServer/routes/web.ts index b0eccb6..fc0675f 100644 --- a/services/request/src/server/services/WebServer/routes/web.ts +++ b/services/request/src/services/WebServer/routes/web.ts @@ -1,5 +1,5 @@ import { RouteRegisterFactory, MiddlewareMethod } from "@autoplex/webserver"; -import Application from "@server/Application"; +import Application from "../../../Application"; export default function register(factory: RouteRegisterFactory, Application>, app: Application) { diff --git a/services/request/src/server/services/WebServer/validators.ts b/services/request/src/services/WebServer/validators.ts similarity index 100% rename from services/request/src/server/services/WebServer/validators.ts rename to services/request/src/services/WebServer/validators.ts diff --git a/services/request/src/services/index.ts b/services/request/src/services/index.ts new file mode 100644 index 0000000..15fe2be --- /dev/null +++ b/services/request/src/services/index.ts @@ -0,0 +1,13 @@ +export { IpcClient as PlexIpc } from "@autoplex-api/plex"; +export { IpcClient as SeekerIpc } from "@autoplex-api/seeker"; +export { IpcClient as SearchIpc } from "@autoplex-api/search"; +export { DatabaseService } from "@autoplex/database"; +import DiscordBot from "./DiscordBot"; +import MovieSearch from "./MovieSearch"; +import WebServer from "./WebServer/WebServer"; + +export { + // DiscordBot, + MovieSearch, + WebServer +} diff --git a/services/request/tsconfig.json b/services/request/tsconfig.json index 181d901..856200a 100644 --- a/services/request/tsconfig.json +++ b/services/request/tsconfig.json @@ -2,11 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ - "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - "@common/*": ["common/*"], - "@lib/*" : ["lib/*"], - "@server/*": ["server/*"] - }, "sourceMap": true, /* Generates corresponding '.map' file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ "typeRoots": ["./src/typings"], /* List of folders to include type definitions from. */