diff --git a/.env.example b/.env.example
index e83c955..c30dcaf 100644
--- a/.env.example
+++ b/.env.example
@@ -42,10 +42,12 @@ WEBSERVER_PORT = 3200
# The URL to the Plex server
PLEX_URL = https://plex.dlii.tech
+# The hash of the Plex server
+PLEX_SERVER_ID =
+
# The X-Plex-Token for the Plex
PLEX_TOKEN_FILE = /run/secrets/plex_token
-
# Library Keys
PLEX_LIBRARY_MOVIES_KEY =
PLEX_LIBRARY_TV_KEY =
diff --git a/package.json b/package.json
index bcee839..ede7ba1 100644
--- a/package.json
+++ b/package.json
@@ -36,12 +36,14 @@
"vue-router": "^4.0.6",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0",
- "websocket": "^1.0.33"
+ "websocket": "^1.0.33",
+ "xml2js": "^0.4.23"
},
"devDependencies": {
"@types/bcrypt": "^3.0.1",
"@types/jsonwebtoken": "^8.5.1",
"@types/node-ipc": "^9.1.3",
+ "@types/xml2js": "^0.4.8",
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"@zerollup/ts-transform-paths": "^1.7.18",
diff --git a/src/app/assets/plex_logo.svg b/src/app/assets/plex_logo.svg
new file mode 100644
index 0000000..b499811
--- /dev/null
+++ b/src/app/assets/plex_logo.svg
@@ -0,0 +1 @@
+
diff --git a/src/app/components/modals/MovieModal.vue b/src/app/components/modals/MovieModal.vue
index 866315f..390f2ea 100644
--- a/src/app/components/modals/MovieModal.vue
+++ b/src/app/components/modals/MovieModal.vue
@@ -24,9 +24,11 @@
{{movie?.overview}}
-
-
@@ -39,9 +41,11 @@
{{ movie.overview }}
- View Request Status
- Watch Now
+ View Request Status
+ {{isRequesting ? "Requesting..." : "Request"}}
diff --git a/src/common/api_schema.ts b/src/common/api_schema.ts
index 5d5ced1..3598772 100644
--- a/src/common/api_schema.ts
+++ b/src/common/api_schema.ts
@@ -42,7 +42,7 @@ export interface IApiPaginatedResponse {
* A movie listing returned from the API
*/
export interface IApiMovie {
- isOnPlex : boolean,
+ plexLink : string | null,
posterPath : string | null,
releaseDate: string | null,
ticketId : number | null,
diff --git a/src/lib/plex/index.ts b/src/lib/plex/index.ts
index 3cc9016..97eefb9 100644
--- a/src/lib/plex/index.ts
+++ b/src/lib/plex/index.ts
@@ -1,6 +1,14 @@
-import { LibraryType, IRawLibrariesResponse } from "./schema";
-import ApiRequestManager from "./request";
+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
@@ -24,6 +32,17 @@ export default class Plex
*/
public async movies(key: string|number) {
let response = await this.requestManager.get(`/library/sections/${key}/all`);
- return new Set(response.match(/(?<=guid="com\.plexapp\.agents\.imdb\:\/\/)tt\d+/ig));
+ 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/src/lib/plex/schema.ts b/src/lib/plex/schema.ts
index 934c95e..26a41ae 100644
--- a/src/lib/plex/schema.ts
+++ b/src/lib/plex/schema.ts
@@ -1,34 +1,12 @@
-/**
- * The supported library types
- */
-export enum LibraryType {
- Movie = "movie",
- TvShow = "show"
-}
-
-/**
- * Library directory informaiton
- */
-export interface IDirectory {
+export interface IVideo {
$: {
- key : string,
- type: LibraryType
+ ratingKey: string,
+ guid: string
}
}
-/**
- * The response generated from a library listing request
- */
-export interface IRawLibrariesResponse {
+export interface ILibraryContents {
MediaContainer: {
- Directory: IDirectory[]
+ Video: IVideo[]
}
}
-
-/**
- * The parsed library type
- */
-export interface ILibrary {
- key : string,
- type: LibraryType
-}
diff --git a/src/server/database/entities/PlexMovie.ts b/src/server/database/entities/PlexMovie.ts
index 52c1bd5..28b924e 100644
--- a/src/server/database/entities/PlexMovie.ts
+++ b/src/server/database/entities/PlexMovie.ts
@@ -10,11 +10,14 @@ export class PlexMovie extends BaseEntity
@Column({ type: "int", nullable: true, unique: true })
tmdbId!: number|null;
+ @Column()
+ plexKey!: number;
+
/**
* Check if a movie is on Plex given its TMDb ID
*/
- public static async isOnPlex(tmdbId: number|string) {
- return await PlexMovie.count({ where: { tmdbId } }) > 0;
+ public static async findPlexKey(tmdbId: number|string) {
+ return (await PlexMovie.findOne({ where: { tmdbId } }))?.plexKey ?? null;
}
/**
@@ -30,11 +33,11 @@ export class PlexMovie extends BaseEntity
/**
* Insert a set of IMDb IDs into the database
*/
- public static async insertImdbSet(imdbIds: Set) {
+ public static async insertMovies(movies: { imdbId: string, plexKey: number }[]) {
await PlexMovie.createQueryBuilder()
.insert()
.into("plex_movie")
- .values([...imdbIds].map(imdbId => ({ imdbId, tmdbId: null })))
+ .values([...movies].map(({imdbId, plexKey}) => ({ imdbId, tmdbId: null, plexKey })))
.execute();
}
diff --git a/src/server/database/entities/User.ts b/src/server/database/entities/User.ts
index 1910bf1..8a5007d 100644
--- a/src/server/database/entities/User.ts
+++ b/src/server/database/entities/User.ts
@@ -102,7 +102,7 @@ export class User extends BaseEntity
relations: ["info"]
});
return tickets.map(ticket => ({
- isOnPlex : false,
+ plexLink : null,
posterPath : ticket.info?.posterPath,
releaseDate: ticket.info?.releaseDate,
ticketId : ticket.id,
diff --git a/src/server/services/MovieSearch.ts b/src/server/services/MovieSearch.ts
index ebf7a4e..b494c4f 100644
--- a/src/server/services/MovieSearch.ts
+++ b/src/server/services/MovieSearch.ts
@@ -1,6 +1,6 @@
import Application from "@server/Application";
import TheMovieDb, { ExternalSource } from "@lib/tmdb";
-import { env, secret } from "@server/util";
+import { env, plexMediaUrl, secret } from "@server/util";
import { request } from "https";
import Service from "./Service";
import TvDb from "./TvDb";
@@ -129,17 +129,18 @@ export default class MovieSearch extends Service
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,
- isOnPlex : await PlexMovie.isOnPlex(id),
requestedBy : (ticket ? {
id : ticket.user.id,
isAdmin: ticket.user.isAdmin,
@@ -163,13 +164,13 @@ export default class MovieSearch extends Service
let movieFetchRequest = this.tmdb.searchMovie(query, year);
let activeTickets = await MovieTicket.activeTicketMap();
let results = await movieFetchRequest;
- let isOnPlex = await Promise.all(results.results.map(
- movie => PlexMovie.isOnPlex(movie.id)
+ let plexKeys = await Promise.all(results.results.map(
+ movie => PlexMovie.findPlexKey(movie.id)
));
return >{
page: results.page,
results: results.results.map((movie, index) => {
- isOnPlex : isOnPlex[index],
+ plexLink : plexKeys[index] !== null ? plexMediaUrl(plexKeys[index]) : null,
posterPath : movie.poster_path,
releaseDate: movie.release_date,
ticketId : activeTickets[movie.id] ?? null,
diff --git a/src/server/services/PlexLibrary.ts b/src/server/services/PlexLibrary.ts
index aa202a8..6301fb9 100644
--- a/src/server/services/PlexLibrary.ts
+++ b/src/server/services/PlexLibrary.ts
@@ -80,12 +80,12 @@ export default class PlexLibrary extends Service
]);
// Calculate the updates
- let toInsert = new Set([...newIds].filter(id => !currentIds.has(id)));
- let toRemove = new Set([...currentIds].filter(id => !newIds.has(id)));
+ let toInsert = new Set(Object.keys(newIds).filter(id => !currentIds.has(id)));
+ let toRemove = new Set([...currentIds].filter(id => !(id in newIds)));
// Remove ald Ids and insert new ones
await PlexMovie.removeImdbSet(toRemove);
- await PlexMovie.insertImdbSet(toInsert);
+ await PlexMovie.insertMovies([...toInsert].map(imdbId => ({imdbId, plexKey: newIds[imdbId]})));
// Update TMDb IDs
for (let movie of await PlexMovie.find({ where: { tmdbId: null } })) {
diff --git a/src/server/util.ts b/src/server/util.ts
index dcb2483..f32b0e4 100644
--- a/src/server/util.ts
+++ b/src/server/util.ts
@@ -57,3 +57,11 @@ export function generateToken(size: number = 48) {
});
});
}
+
+/**
+ * Generate a URL to a movie/TV show on Plex
+ */
+export function plexMediaUrl(plexKey: number) {
+ return `${env("PLEX_URL")}/web/index.html#!/server/${env("PLEX_SERVER_ID")}/` +
+ `details?key=%2Flibrary%2Fmetadata%2F${plexKey}&context=library%3Acontent.library;`;
+}
diff --git a/yarn.lock b/yarn.lock
index 803be50..6e32f83 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -142,6 +142,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
+"@types/xml2js@^0.4.8":
+ version "0.4.8"
+ resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a"
+ integrity sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA==
+ dependencies:
+ "@types/node" "*"
+
"@types/zen-observable@^0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"