Browse Source

Rewrite movie list. Returned movies contain ticket IDs if available. Movie details contain the requested user if available. Added admin stuff. Added active requests to dashboard

master
David Ludwig 4 years ago
parent
commit
c913340bd1
14 changed files with 295 additions and 115 deletions
  1. +36
    -0
      src/app/components/MovieList.vue
  2. +1
    -0
      src/app/components/SideNav.vue
  3. +36
    -30
      src/app/components/modals/MovieModal.vue
  4. +10
    -1
      src/app/routes/index.ts
  5. +5
    -8
      src/app/store/actions.ts
  6. +31
    -4
      src/app/views/Dashboard.vue
  7. +18
    -29
      src/app/views/Search.vue
  8. +60
    -10
      src/common/api_schema.ts
  9. +1
    -1
      src/lib/tmdb/index.ts
  10. +0
    -3
      src/server/database/entities/MovieInfo.ts
  11. +23
    -7
      src/server/database/entities/MovieTicket.ts
  12. +19
    -0
      src/server/database/entities/User.ts
  13. +44
    -21
      src/server/services/MovieSearch.ts
  14. +11
    -1
      src/server/services/WebServer/routes/api.ts

+ 36
- 0
src/app/components/MovieList.vue View File

@ -0,0 +1,36 @@
<template>
<ul class="w-full grid gap-8 grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 self-center ">
<li class="inline-block" v-for="(movie, index) in movies">
<movie-poster class="shadow-md hover:shadow-lg rounded-xl motion-safe:transform hover:scale-105 transition-transform ease-out cursor-pointer"
:src="movie.posterPath ?? undefined" size="w185" @click="$emit('onClickMovie', movie, index)"/>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiMovie } from "../../common/api_schema";
import MoviePoster from "./MoviePoster.vue";
export default defineComponent({
components: {
MoviePoster
},
data() {
return {
}
},
methods: {
onModalClosed() {
let parentPath = this.$route.path.split('/').slice(0, -1).join('/');
this.$router.push({ path: parentPath, query: this.$route.query });
}
},
props: {
movies: {
default: <IApiMovie[]>[]
}
}
});
</script>

+ 1
- 0
src/app/components/SideNav.vue View File

@ -64,6 +64,7 @@ nav > .links a {
}
nav > .links .router-link-active {
@apply bg-indigo-500 text-white shadow-md hover:bg-indigo-500 cursor-default;
}
nav > .links span {
@apply w-5


+ 36
- 30
src/app/components/modals/MovieModal.vue View File

@ -1,13 +1,15 @@
<template>
<transition appear name="fade">
<div class="modal p-8 overflow-auto items-center" @click.self="close">
<transition name="slide">
<div v-if="tmdbId !== undefined" class="modal p-8 overflow-auto items-center" @click.self="close">
<transition name="slide" appear
@before-enter="lockScroll(true)" @enter-cancelled="lockScroll(false)"
@leave="lockScroll(false)" @leave-cancelled="lockScroll(true)">
<div v-if="movie !== undefined" class="relative w-full max-w-6xl bg-white md:bg-none overflow-hidden rounded-xl flex-col flex-shrink-0 shadow-xl">
<div class="bg-center bg-cover" :class="{ 'text-white': isDark }"
:style="movie.backdrop_path ? `background: url('/api/tmdb/image/w1280${movie.backdrop_path}')` : ''">
:style="movie.backdropPath ? `background: url('/api/tmdb/image/w1280${movie.backdropPath}')` : ''">
<div class="movie-modal-content z-10" :style="backdropOverlayStyle">
<div class="flex p-4 items-center">
<movie-poster v-if="movie.poster_path" :src="movie.poster_path"
<movie-poster v-if="movie.posterPath" :src="movie.posterPath"
class="w-3/12 flex-shrink-0 rounded-xl shadow-md md:shadow-xl" @onLoad="onPosterLoad"/>
<div class="p-4 flex flex-col justify-center">
<h2 class="text-lg md:text-4xl">
@ -23,9 +25,9 @@
</div>
<div class="mt-4 hidden md:block">
<button class="py-2 px-4 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-if="movie.is_requested">View Request Status</button>
v-if="movie.ticketId !== null">View Request Status</button>
<button v-else class="py-2 w-36 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
:disabled="isRequesting || isRequested" @click="request">{{isRequesting ? "Requesting..." : isRequested ? "Request Sent" : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
</div>
</div>
</div>
@ -38,9 +40,9 @@
</div>
<div class="text-center">
<button class="py-2 px-4 rounded-full bg-red-500 text-white disabled:opacity-50"
v-if="movie.is_requested">View Request Status</button>
v-if="movie.ticketId !== null">View Request Status</button>
<button v-else class="py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 disabled:bg-black"
:disabled="isRequesting || isRequested" @click="request">{{isRequesting ? "Requesting..." : isRequested ? "Request Sent" : "Request"}}</button>
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
</div>
</div>
</div>
@ -51,7 +53,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { IApiMovieDetailsResponse, IApiMovieDetails } from "@common/api_schema";
import { IApiDataResponse, IApiMovieDetails } from "@common/api_schema";
import { authFetch } from "../../routes";
import { getAverageRgb } from "../../util";
import { useStore, Mutation, Action } from "../../store";
@ -79,16 +81,16 @@ export default defineComponent({
// return L <= 0.179;
},
releaseYear(): string {
if (!this.movie || !this.movie.release_date) {
if (!this.movie || !this.movie.releaseDate) {
return "";
}
return this.movie.release_date.slice(0, 4);
return this.movie.releaseDate.slice(0, 4);
},
releaseDate(): string {
if (!this.movie || !this.movie.release_date) {
if (!this.movie || !this.movie.releaseDate) {
return "";
}
let [year, month, day] = this.movie.release_date.split('-');
let [year, month, day] = this.movie.releaseDate.split('-');
return `${month}/${day}/${year}`;
},
runtime(): string {
@ -105,11 +107,13 @@ export default defineComponent({
rgb: {r: 0, g: 0, b: 0},
movie: <IApiMovieDetails|undefined>undefined,
isPosterLoaded: false,
isRequesting: false,
isRequested: false
isRequesting: false
}
},
methods: {
lockScroll(lock: boolean) {
useStore().commit(Mutation.LockScroll, lock);
},
close() {
this.movie = undefined;
this.$emit("onClose");
@ -148,22 +152,30 @@ export default defineComponent({
this.rgb = <any>rgb;
},
async fetchMovieDetails() {
let response = await (authFetch(`/api/movie/details/${this.movieId}`));
if (this.tmdbId === undefined) {
return;
}
let response = await (authFetch(`/api/movie/details/${this.tmdbId}`));
if (response.status != 200) {
this.close();
return;
}
let movie = <IApiMovieDetailsResponse> await response.json();
let movie = <IApiDataResponse<IApiMovieDetails>> await response.json();
this.movie = movie.data;
},
async request() {
if (this.isRequesting || this.movie == null || this.movie.imdb_id == null || this.isRequested) {
if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) {
return;
}
this.isRequesting = true;
let ticketId = await this.$store.dispatch(Action.RequestMovie, this.movieId);
this.isRequested = true;
let response = await this.$store.dispatch(Action.RequestMovie, this.movie.tmdbId);
this.isRequesting = false;
if (response.status == "Forbidden") {
console.log("Failed to add movie: quota has been met");
return;
}
console.log(response);
this.movie.ticketId = response.data.ticketId;
}
},
mounted() {
@ -174,21 +186,15 @@ export default defineComponent({
document.removeEventListener("keydown", this.onKeyPress);
},
props: {
movieId: {
tmdbId: {
type: [Number, String],
required: true
required: false
}
},
watch: {
movieId(newId: number, oldId: number) {
console.log(newId);
tmdbId(newId: number|string|undefined, oldId: number|string|undefined) {
this.fetchMovieDetails();
}
},
beforeRouteEnter() {
useStore().commit(Mutation.LockScroll, true);
},
beforeRouteLeave() {
useStore().commit(Mutation.LockScroll, false);
}
});
</script>


+ 10
- 1
src/app/routes/index.ts View File

@ -1,3 +1,4 @@
import { IApiDataResponse } from "@common/api_schema";
import { createRouter, createWebHistory, NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
import store, { Action } from "../store";
@ -62,6 +63,14 @@ export async function authFetch(path: string, options: RequestInit = {}): Promis
return response;
}
/**
* Perform an authorized API request returning a JSON object
*/
export async function authFetchApi<T>(path: string, options: RequestInit = {}) {
let response = await authFetch(path, options);
return <Promise<IApiDataResponse<T>>>response.json();
}
const routes: RouteRecordRaw[] = [
{
path: "/",
@ -81,7 +90,7 @@ const routes: RouteRecordRaw[] = [
beforeEnter: requiresAuth,
children: [
{
path: ":movieId",
path: ":tmdbId",
name: "Lookup",
component: () => import("../components/modals/MovieModal.vue"),
props: true


+ 5
- 8
src/app/store/actions.ts View File

@ -2,8 +2,9 @@ import { ActionTree } from "vuex";
import { Actions } from "./generics";
import { IState } from "./state";
import { Mutation, MutationsTypes } from "./mutations";
import router, { authFetch } from "../routes";
import router, { authFetch, authFetchApi } from "../routes";
import { GettersTypes } from "./getters";
import { IApiDataResponse } from "@common/api_schema";
// Payload types
type IAuthenticatePayload = { email: string, password: string, remember: boolean };
@ -35,7 +36,7 @@ export type ActionsTypes = {
// RESTful
[Action.AuthFetch] : (payload: IAuthFetchPayload) => Promise<Response>,
[Action.RequestMovie]: (tmdbId: number | string) => Promise<number>
[Action.RequestMovie]: (tmdbId: number | string) => Promise<IApiDataResponse<{ ticketId: number }>>
}
/**
@ -125,11 +126,7 @@ export const actions: Actions<IState, GettersTypes, MutationsTypes, ActionsTypes
/**
* Request a movie
*/
async [Action.RequestMovie](_, tmdbId): Promise<number> {
let response = await authFetch(`/api/movie/request/create/tmdb/${tmdbId}`);
if (response.status != 200) {
throw new Error("Movie request failed");
}
return (await response.json()).data.ticket_id;
async [Action.RequestMovie](_, tmdbId) {
return await authFetchApi(`/api/movie/request/create/tmdb/${tmdbId}`);
}
};

+ 31
- 4
src/app/views/Dashboard.vue View File

@ -1,16 +1,43 @@
<template>
<div class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<movie-poster src=""/>
<div class="h-12 text-2xl flex items-center">
<h1 class="">Active Requests</h1>
</div>
<movie-list :movies="activeRequests" @onClickMovie="displayMovie"/>
<movie-modal :tmdb-id="activeTmdb" @onClose="activeTmdb = undefined"/>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import MoviePoster from "../components/MoviePoster.vue";
import { authFetchApi } from "../routes";
import MovieList from "../components/MovieList.vue";
import MovieModal from "../components/modals/MovieModal.vue";
import { IApiMovie } from "../../common/api_schema";
export default defineComponent({
components: {
MoviePoster
MovieList,
MovieModal
},
data() {
return {
activeRequests: <IApiMovie[]>[],
activeTmdb: <number|undefined> undefined
}
},
methods: {
displayMovie(movie: IApiMovie, index: number) {
this.activeTmdb = movie.tmdbId;
},
fetchRequests() {
return authFetchApi<IApiMovie[]>("/api/movie/request/tickets/active").then((response) => {
this.activeRequests = response.data;
}).catch(() => {
console.error("Failed to fetch active movie tickets");
});
}
},
mounted() {
this.fetchRequests();
}
});
</script>

+ 18
- 29
src/app/views/Search.vue View File

@ -6,43 +6,25 @@
<button class="py-3 px-6 bg-indigo-500 text-white" type="submit">Search</button>
</div>
</form>
<ul class="w-full grid gap-8 grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10 self-center ">
<li class="inline-block" v-for="(movie, index) in movies">
<router-link :to="{ name: 'Lookup', params: { movieId: movie.id }, query: $route.query }">
<movie-poster class="shadow-md hover:shadow-lg rounded-xl motion-safe:transform hover:scale-105 transition-transform ease-out"
:src="movie.poster_path ?? undefined" size="w185"/>
<!-- <div class="w-full h-full flex shadow-md hover:shadow-lg bg-gray-300 text-gray-500 text-4xl overflow-hidden rounded-xl cursor-pointer motion-safe:transform hover:scale-105 transition-transform ease-out">
<i v-if="!movie.poster_path" class="fas fa-image mx-auto my-auto"></i>
<img v-else class="w-full h-full object-cover" loading="lazy" :ref="`poster-${index}`"
:src="`/api/tmdb/image/w185${movie.poster_path}`">
</div> -->
</router-link>
</li>
</ul>
<router-view @onClose="modalClosed"></router-view>
<!-- <movie-modal v-if="activeMovie != -1" :movie="movies[activeMovie]" :img-elem="$refs[`poster-${activeMovie}`]"/> -->
<movie-list :movies="movies" @onClickMovie="displayMovie"/>
<router-view @onClose="onModalClosed"></router-view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiDataResponse } from "@common/api_schema";
import { IMovieSearchResult, IPaginatedResponse } from "@lib/tmdb/schema";
import { authFetch } from "../routes";
import MovieModal from "../components/modals/MovieModal.vue";
import MoviePoster from "../components/MoviePoster.vue";
type MovieResults = IApiDataResponse<IPaginatedResponse<IMovieSearchResult>>;
import { IApiMovie, IApiPaginatedResponse } from "@common/api_schema";
import { authFetchApi } from "../routes";
import MovieList from "../components/MovieList.vue";
export default defineComponent({
components: {
MovieModal,
MoviePoster
MovieList
},
data() {
return {
exampleTitles: ["John Wick", "The Incredibles 2", "Stand by Me", "Shawshank Redemption", "The Dark Knight", "Pulp Fiction", "Forrest Gump"],
isSubmitting: false,
movies: <IMovieSearchResult[]>[],
movies: <IApiMovie[]>[],
searchValue: <string>this.$route.query["query"] || "",
page: -1,
totalPages: 0,
@ -50,7 +32,14 @@ export default defineComponent({
}
},
methods: {
modalClosed() {
displayMovie(movie: IApiMovie) {
this.$router.push({
name : "Lookup",
params: { tmdbId: movie.tmdbId },
query : this.$route.query
});
},
onModalClosed() {
this.$router.push({ name: "Search", query: this.$route.query });
},
async search(pushRoute: boolean = true) {
@ -65,11 +54,11 @@ export default defineComponent({
}
this.isSubmitting = true;
try {
let response = <MovieResults> await (authFetch(`/api/movie/search?query=${encodeURI(this.searchValue)}`).then(response => response.json()));
let response = await authFetchApi<IApiPaginatedResponse<IApiMovie>>(`/api/movie/search?query=${encodeURI(this.searchValue)}`);
this.movies = response.data.results;
this.page = response.data.page;
this.totalPages = response.data.total_pages;
this.totalResults = response.data.total_results;
this.totalPages = response.data.totalPages;
this.totalResults = response.data.totalResults;
console.log("Got results", this.totalResults);
} catch(e) {
console.log("Error fetching movies:", e);


+ 60
- 10
src/common/api_schema.ts View File

@ -1,12 +1,18 @@
import { IMovieDetails } from "@lib/tmdb/schema";
import { IMovieDetails, IPaginatedResponse } from "@lib/tmdb/schema";
/**
* The JWT auth token structure
* Basic user information schema
*/
export interface ITokenSchema {
export interface IUser {
id : number,
name : string,
isAdmin: boolean,
isAdmin: boolean
}
/**
* The JWT auth token structure
*/
export interface ITokenSchema extends IUser {
iat : number,
exp : number
}
@ -25,16 +31,60 @@ export interface IApiDataResponse<T> extends IApiResponse {
data: T
}
export interface IApiPaginatedResponse<T> {
page : number,
results : T[],
totalPages : number,
totalResults: number
};
/**
* Movie detail data
* A movie listing returned from the API
*/
export interface IApiMovie {
isOnPlex : boolean,
posterPath : string | null,
releaseDate: string | null,
ticketId : number | null,
title : string,
tmdbId : number
}
/**
* Movie details returned from the API
*/
export interface IApiMovieDetails extends Pick<IMovieDetails, "title" | "overview" | "runtime" |
"release_date" | "imdb_id" | "backdrop_path" | "poster_path">
{
is_requested: boolean
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<IMovieDetails, "title" | "overview" | "runtime" |
// "release_date" | "imdb_id" | "backdrop_path" | "poster_path">
// {
// is_requested: boolean
// }
/**
*
*/
export type IApiMovieDetailsResponse = IApiDataResponse<IApiMovieDetails>;
// export type IApiMovieDetailsResponse = IApiDataResponse<IApiMovieDetails>;
// 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<IApiMovieTicket[]>;

+ 1
- 1
src/lib/tmdb/index.ts View File

@ -14,7 +14,7 @@ export default class TheMovieDb
}
public async searchMovie(query: string, year?: number, page?: number) {
return await this.requestManager.get<Schema.IMovieSearchResult>("/search/movie", { query, year });
return await this.requestManager.get<Schema.IPaginatedResponse<Schema.IMovieSearchResult>>("/search/movie", { query, year });
}
public async movie(id: number) {


+ 0
- 3
src/server/database/entities/MovieInfo.ts View File

@ -6,9 +6,6 @@ export class MovieInfo extends BaseEntity
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true })
tmdbId!: number;
@Column({ type: "text", nullable: true })
overview!: string | null;


+ 23
- 7
src/server/database/entities/MovieTicket.ts View File

@ -1,5 +1,5 @@
import { IApiMovieDetails } from "@common/api_schema";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn } from "typeorm";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn, Not, IsNull } from "typeorm";
import { MovieInfo } from "./MovieInfo";
import { MovieTorrent } from "./MovieTorrent";
import { User } from "./User";
@ -10,6 +10,9 @@ export class MovieTicket extends BaseEntity
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "int", nullable: true })
tmdbId!: number | null;
@Column({ type: "varchar", length: 27, nullable: true })
imdbId!: string | null;
@ -41,6 +44,19 @@ export class MovieTicket extends BaseEntity
@JoinColumn()
info!: MovieInfo | null;
/**
* @TODO This needs to check for fulfilled tickets too, but not there yet
* Fetch all active ticket ID's
*/
public static async activeTicketMap() {
let tickets = await MovieTicket.find({ where: { tmdbId: Not(IsNull()), isCanceled: false } });
let result: {[tmdbId: number]: number} = {};
for (let ticket of tickets) {
result[<number>ticket.tmdbId] = ticket.id;
}
return result;
}
/**
* Insert a request via IMDb movie details
*/
@ -59,17 +75,17 @@ export class MovieTicket extends BaseEntity
public static async requestTmdb(user: User, tmdbId: number, movie: IApiMovieDetails) {
let info = new MovieInfo();
info.overview = movie.overview;
info.posterPath = movie.poster_path;
info.backdropPath = movie.backdrop_path;
info.releaseDate = movie.release_date;
info.posterPath = movie.posterPath;
info.backdropPath = movie.backdropPath;
info.releaseDate = movie.releaseDate;
info.runtime = movie.runtime;
info.tmdbId = tmdbId;
await info.save();
let ticket = new MovieTicket();
ticket.imdbId = movie.imdb_id;
ticket.tmdbId = tmdbId;
ticket.imdbId = movie.imdbId;
ticket.title = movie.title;
ticket.year = parseInt(movie.release_date.slice(0, 4));
ticket.year = movie.releaseDate ? parseInt(movie.releaseDate.slice(0, 4)) : null;
ticket.user = user;
ticket.info = info;
return await ticket.save();


+ 19
- 0
src/server/database/entities/User.ts View File

@ -4,6 +4,7 @@ import jwt from "jsonwebtoken";
import { MovieTicket } from "./MovieTicket";
import Application from "@server/Application";
import { MovieQuota } from "./MovieQuota";
import { IApiMovie } from "@common/api_schema";
@Entity()
export class User extends BaseEntity
@ -97,4 +98,22 @@ export class User extends BaseEntity
let user = <User>await User.findOne(this.id, { relations: ["quota"] });
return user.quota;
}
/**
* Fetch
*/
public async activeMovieTickets() {
let tickets = await MovieTicket.find({
where: { user: this, isCanceled: false, isFulfilled: false },
relations: ["info"]
});
return tickets.map(ticket => <IApiMovie>({
isOnPlex : false,
posterPath : ticket.info?.posterPath,
releaseDate: ticket.info?.releaseDate,
ticketId : ticket.id,
title : ticket.title,
tmdbId : ticket.tmdbId
}));
}
}

+ 44
- 21
src/server/services/MovieSearch.ts View File

@ -4,8 +4,8 @@ import { env, secret } from "@server/util";
import { request } from "https";
import Service from "./Service";
import TvDb from "./TvDb";
import { IApiMovieDetails } from "@common/api_schema";
import { MovieInfo } from "@server/database/entities";
import { IApiMovie, IApiMovieDetails, IApiPaginatedResponse } from "@common/api_schema";
import { MovieTicket } from "@server/database/entities";
const CACHE_CLEAR_INTERVAL = 1000*60; // 60 seconds
@ -119,22 +119,36 @@ export default class MovieSearch extends Service
*/
public async details(id: number) {
if (id in this.movieCache) {
if (!this.movieCache[id].movie.is_requested) {
let isRequested = await MovieInfo.count({tmdbId: id}) > 0;
this.movieCache[id].movie.is_requested = isRequested;
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 movie = await this.tmdb.movie(id);
let fetchMovieRequest = this.tmdb.movie(id);
let ticket = await MovieTicket.findOne({ where: { tmdbId: id, isCanceled: false }, relations: ["user"] });
let movie = await fetchMovieRequest;
let result: IApiMovieDetails = {
backdrop_path: movie.backdrop_path,
imdb_id : movie.imdb_id,
overview : movie.overview,
poster_path : movie.poster_path,
release_date : movie.release_date,
runtime : movie.runtime,
title : movie.title,
is_requested : await MovieInfo.count({tmdbId: id}) > 0
tmdbId : id,
backdropPath: movie.backdrop_path,
imdbId : movie.imdb_id,
overview : movie.overview,
posterPath : movie.poster_path,
releaseDate : movie.release_date,
runtime : movie.runtime,
title : movie.title,
ticketId : ticket?.id ?? null,
isOnPlex : false,
requestedBy : (ticket ? {
id : ticket.user.id,
isAdmin: ticket.user.isAdmin,
name : ticket.user.name
} : null)
};
return this.cacheMovie(id, result).movie;
}
@ -143,12 +157,21 @@ export default class MovieSearch extends Service
* Search for a movie
*/
public async search(query: string, year?: number) {
return await this.tmdb.searchMovie(query, year);
// let results = await this.tvdb.searchMovie(query, year);
// return results.map(movie => <any>{
// image : movie.image_url ? `/api/tvdb/artwork${new URL(movie.image_url).pathname}`: null,
// name : movie.name,
// year : movie.year
// });
let movieFetchRequest = this.tmdb.searchMovie(query, year);
let activeTickets = await MovieTicket.activeTicketMap();
let results = await movieFetchRequest;
return <IApiPaginatedResponse<IApiMovie>>{
page: results.page,
results: results.results.map(movie => <IApiMovie>{
isOnPlex : false,
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
};
}
}

+ 11
- 1
src/server/services/WebServer/routes/api.ts View File

@ -13,6 +13,7 @@ import RequestTmdbMovieRequest from "../requests/RequestTmdbMovieRequest";
export default function register(factory: RouteRegisterFactory, app: Application) {
factory.prefix("/api", [auth], (factory, app) => {
/**
* Service proxies
*/
@ -46,13 +47,22 @@ export default function register(factory: RouteRegisterFactory, app: Application
// Movie Request ---------------------------------------------------------------------------
factory.prefix("/movie/request", [], (factory, app) => {
/**
* Fetch a user's active movie tickets
*/
factory.get("/tickets/active", async (request, reply) => {
let user = request.middlewareParams.auth.user;
let tickets = await user.activeMovieTickets();
return reply.send({ status: "Success", data: tickets });
});
/**
* Request a movie to download
*/
factory.get("/create/tmdb/:tmdb_id", handle([RequestTmdbMovieRequest], async (request, reply) => {
// Verify that the ID has not yet been requested
let tmdbId = (<any>request.params)["tmdb_id"];
if (0 != await MovieInfo.count({tmdbId})) {
if (0 != await MovieTicket.count({ where: { tmdbId, isCanceled: false } })) {
reply.status(409);
reply.send({ status: "Conflict" });
return;


Loading…
Cancel
Save