diff --git a/backend/migrations/1739757875787-clear-metadata-cache.ts b/backend/migrations/1739757875787-clear-metadata-cache.ts new file mode 100644 index 0000000..c237d3e --- /dev/null +++ b/backend/migrations/1739757875787-clear-metadata-cache.ts @@ -0,0 +1,10 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ClearMetadataCache1739757875787 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Drop all rows in SERIES table + await queryRunner.query('DELETE FROM "series"'); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/package.json b/backend/package.json index cc05b2c..e98422e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,7 @@ "typeorm": "ts-node ./node_modules/typeorm/cli", "typeorm:run-migrations": "npm run typeorm migration:run -- -d ./dist/data-source.js", "typeorm:generate-migration": "ts-node ./node_modules/typeorm/cli -d ./dist/data-source.js migration:generate", - "typeorm:create-migration": "ts-node ./node_modules/typeorm/cli migration:create ./migrations/$npm_config_name", + "typeorm:create-migration": "ts-node ./node_modules/typeorm/cli migration:create", "typeorm:revert-migration": "ts-node ./node_modules/typeorm/cli -d ./dist/data-source.js migration:revert" }, "dependencies": { diff --git a/backend/src/main.ts b/backend/src/main.ts index 48152b8..383869d 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -24,7 +24,14 @@ async function createAdminUser(userService: UsersService) { } async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: [ + 'error', + 'warn', + 'log', + ...(ENV === 'development' ? (['debug'] as const) : []), + ], + }); app.setGlobalPrefix('api'); app.enableCors(); app.use(json({ limit: '50mb' })); diff --git a/backend/src/metadata/metadata.module.ts b/backend/src/metadata/metadata.module.ts index eb045a9..bd64432 100644 --- a/backend/src/metadata/metadata.module.ts +++ b/backend/src/metadata/metadata.module.ts @@ -1,12 +1,25 @@ +import { CacheModule } from '@nestjs/cache-manager'; import { Module } from '@nestjs/common'; -import { DatabaseModule } from 'src/database/database.module'; +import { TMDB_CACHE_TTL } from 'src/consts'; +import { UsersModule } from 'src/users/users.module'; import { metadataProviders } from './metadata.providers'; import { MetadataService } from './metadata.service'; -import { TmdbModule } from './tmdb/tmdb.module'; +import { TmdbController } from './tmdb/tmdb.controller'; +import { tmdbProviders } from './tmdb/tmdb.providers'; +import { TmdbService } from './tmdb/tmdb.service'; @Module({ - imports: [TmdbModule], - providers: [...metadataProviders, MetadataService], + imports: [ + UsersModule, + CacheModule.register({ ttl: TMDB_CACHE_TTL, max: 10_000 }), + ], + providers: [ + ...metadataProviders, + MetadataService, + ...tmdbProviders, + TmdbService, + ], + controllers: [TmdbController], exports: [MetadataService], }) export class MetadataModule {} diff --git a/backend/src/metadata/metadata.service.ts b/backend/src/metadata/metadata.service.ts index 3801904..8356e0f 100644 --- a/backend/src/metadata/metadata.service.ts +++ b/backend/src/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Movie, Series } from './metadata.entity'; import { MOVIE_REPOSITORY, SERIES_REPOSITORY } from './metadata.providers'; @@ -9,6 +9,8 @@ import { TmdbService } from './tmdb/tmdb.service'; @Injectable() export class MetadataService { + private logger = new Logger(MetadataService.name); + constructor( @Inject(TMDB_API) private tmdbApi: TmdbApi, @@ -39,11 +41,7 @@ export class MetadataService { !movie.updatedAt || new Date().getTime() - movie.updatedAt.getTime() > TMDB_CACHE_TTL ) { - const tmdbMovie = await this.tmdbApi.v3 - .movieDetails(Number(tmdbId), { - append_to_response: 'videos,credits,external_ids,images', - }) - .then((r) => r.data as TmdbMovieFull); + const tmdbMovie = await this.tmdbService.getFullMovie(Number(tmdbId)); movie.tmdbMovie = tmdbMovie; } @@ -65,7 +63,7 @@ export class MetadataService { } if (series.isStale()) { - console.log('getting metadata for series', tmdbId); + this.logger.debug(`Caching series ${tmdbId}`); const tmdbSeries = await this.tmdbService.getFullSeries(Number(tmdbId)); if (tmdbSeries) series.tmdbSeries = tmdbSeries; } diff --git a/backend/src/metadata/tmdb/tmdb.controller.ts b/backend/src/metadata/tmdb/tmdb.controller.ts index 0c3082c..b1ed1f8 100644 --- a/backend/src/metadata/tmdb/tmdb.controller.ts +++ b/backend/src/metadata/tmdb/tmdb.controller.ts @@ -1,55 +1,28 @@ -import { - Cache, - CACHE_MANAGER -} from '@nestjs/cache-manager'; +import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { All, Controller, Inject, + Logger, Param, Req, Res, - UseGuards + UseGuards, } from '@nestjs/common'; import { Request, Response } from 'express'; import { GetAuthUser, UserAccessControl } from 'src/auth/auth.guard'; import { TMDB_API_KEY, TMDB_CACHE_TTL } from 'src/consts'; import { User } from 'src/users/user.entity'; +import { MetadataService } from '../metadata.service'; @UseGuards(UserAccessControl) @Controller('tmdb') export class TmdbController { - constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} - // constructor(private metadataService: MetadataService) {} - - // @Get('v3/proxy/3/movie/:tmdbId') - // async getMovieDetails( - // @Param('tmdbId') tmdbId: string, - // @Next() next: NextFunction, - // @GetAuthUser() user: User, - // // @Res({ passthrough: true }) res: Response, // Passthrough required - // ) { - // if (!parseInt(tmdbId)) { - // console.log('Invalid TMDB ID', tmdbId); - // next(); - // return; - // } - - // console.log('getting cached movie', tmdbId); - - // const movie = await this.metadataService - // .getMovieByTmdbId(tmdbId) - // .catch((e) => { - // console.error('Error getting movie by TMDB ID', tmdbId, e); - // return null; - // }); - - // if (!movie?.tmdbMovie) { - // console.error('No movie found for TMDB ID', tmdbId); - // } - // console.log('returning cached movie', tmdbId); - // return movie?.tmdbMovie; - // } + private logger = new Logger(TmdbController.name); + constructor( + @Inject(CACHE_MANAGER) private cacheManager: Cache, + private metadataService: MetadataService, + ) {} // @UseInterceptors(CacheInterceptor) // @CacheTTL(METADATA_CACHE_TTL) @@ -62,7 +35,7 @@ export class TmdbController { ) { const uri = params[0] + '?' + req.url.split('?')[1]; const cached = await this.cacheManager.get(uri).catch((e) => { - console.error('Error getting cache', e); + this.logger.error('Error getting cache', e); return null; }); @@ -72,44 +45,34 @@ export class TmdbController { return cached; } - // console.log('TMDB PROXY', req.url); + // 3/tv/87739?append_to_response=videos%2Caggregate_credits%2Cexternal_ids%2Cimages&include_image_language=en%2Cen%2Cnull + const first = uri.split('?')?.[0]; + if (req.method === 'GET' && first.match(/3\/tv\/\d+$/)) { + const tmdbId = first.split('/').pop(); + this.logger.debug(`Getting series from cache: ${tmdbId}`); + const metadata = await this.metadataService.getSeriesByTmdbId(tmdbId); + res.json(metadata.tmdbSeries); + return metadata; + } else if (req.method === 'GET' && first.match(/3\/movie\/\d+$/)) { + const tmdbId = first.split('/').pop(); + this.logger.debug(`Getting movie from cache: ${tmdbId}`); + const metadata = await this.metadataService.getMovieByTmdbId(tmdbId); + res.json(metadata.tmdbMovie); + return metadata; + } - // if (params[0].match(/^3\/movie\/\d+\/?$/)) { - // // console.log('req.params', req.params); - - // const movie = await this.metadataService.getMovieByTmdbId( - // req.params[0].split('/')[2], - // ); - - // // console.log('movie', movie); - // if (movie?.tmdbMovie) { - // // console.log('returning cached movie'); - // res.json(movie.tmdbMovie); - // return; - // } - // } + this.logger.debug(`TMDB proxy cache miss: ${req.method} ${uri}`); const proxyRes = await fetch(`https://api.themoviedb.org/${uri}`, { method: req.method || 'GET', headers: { Authorization: `Bearer ${TMDB_API_KEY}`, - // ...headers, - // Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`, }, - }) - // .then((r) => { - // // r.text().then((text) => - // // console.log('TMDB Proxy response', uri, r.status, text), - // // ); - // return r; - // }) - .catch((e) => { - console.error('TMDB Proxy error', e); - // res.status(500).send('Proxy error'); - throw e; - }); + }).catch((e) => { + this.logger.error('TMDB Proxy error', e); + throw e; + }); - // Readable.from(proxyRes.body).pipe(res); const json = await proxyRes.json(); res.status(proxyRes.status); res.json(json); diff --git a/backend/src/metadata/tmdb/tmdb.dto.ts b/backend/src/metadata/tmdb/tmdb.dto.ts index 2861907..1d1592d 100644 --- a/backend/src/metadata/tmdb/tmdb.dto.ts +++ b/backend/src/metadata/tmdb/tmdb.dto.ts @@ -12,11 +12,22 @@ export type MovieExternalIds = Awaited< export type MovieImages = Awaited< ReturnType >['data']; - export type TmdbMovie = Awaited< ReturnType >['data']; +export type SeriesVideos = Awaited< + ReturnType +>['data']; +export type SeriesCredits = Awaited< + ReturnType +>['data']; +export type SeriesExternalIds = Awaited< + ReturnType +>['data']; +export type SeriesImages = Awaited< + ReturnType +>['data']; export type TmdbSeries = Awaited< ReturnType >['data']; @@ -28,4 +39,9 @@ export type TmdbMovieFull = TmdbMovie & { images: MovieImages; }; -export type TmdbSeriesFull = TmdbSeries; +export type TmdbSeriesFull = TmdbSeries & { + videos: SeriesVideos; + aggregate_credits: SeriesCredits; + external_ids: SeriesExternalIds; + images: SeriesImages; +}; diff --git a/backend/src/metadata/tmdb/tmdb.module.ts b/backend/src/metadata/tmdb/tmdb.module.ts deleted file mode 100644 index 284c406..0000000 --- a/backend/src/metadata/tmdb/tmdb.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CacheModule } from '@nestjs/cache-manager'; -import { Module } from '@nestjs/common'; -import { TMDB_CACHE_TTL } from 'src/consts'; -import { UsersModule } from 'src/users/users.module'; -import { TmdbController } from './tmdb.controller'; -import { tmdbProviders } from './tmdb.providers'; -import { TmdbService } from './tmdb.service'; - -@Module({ - imports: [ - UsersModule, - CacheModule.register({ ttl: TMDB_CACHE_TTL, max: 10_000 }), - ], - providers: [...tmdbProviders, TmdbService], - exports: [...tmdbProviders, TmdbService], - controllers: [TmdbController], -}) -export class TmdbModule {} diff --git a/backend/src/metadata/tmdb/tmdb.service.ts b/backend/src/metadata/tmdb/tmdb.service.ts index 734b7f1..51b3658 100644 --- a/backend/src/metadata/tmdb/tmdb.service.ts +++ b/backend/src/metadata/tmdb/tmdb.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { TmdbSeriesFull } from './tmdb.dto'; +import { TmdbMovieFull, TmdbSeriesFull } from './tmdb.dto'; import { TMDB_API, TmdbApi } from './tmdb.providers'; @Injectable() @@ -11,8 +11,10 @@ export class TmdbService { async getFullSeries(tmdbId: number): Promise { const tmdbSeries = await this.tmdbApi.v3 - .tvSeriesDetails(Number(tmdbId)) - .then((r) => r.data); + .tvSeriesDetails(Number(tmdbId), { + append_to_response: 'videos,aggregate_credits,external_ids,images', + }) + .then((r) => r.data as TmdbSeriesFull); // .catch((e) => { // console.error('could not get metadata for series', tmdbId, e); // return e; @@ -20,4 +22,12 @@ export class TmdbService { return tmdbSeries; } + + async getFullMovie(tmdbId: number) { + return this.tmdbApi.v3 + .movieDetails(Number(tmdbId), { + append_to_response: 'videos,credits,external_ids,images', + }) + .then((r) => r.data as TmdbMovieFull); + } }