diff --git a/backend/migrations/1743408772473-update-tmdbseries-cache.ts b/backend/migrations/1743408772473-update-tmdbseries-cache.ts new file mode 100644 index 0000000..c2e39f0 --- /dev/null +++ b/backend/migrations/1743408772473-update-tmdbseries-cache.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateTmdbSeriesCacheType1743408772473 implements MigrationInterface { + name = 'UpdateTmdbSeriesCacheType1743408772473'; + + public async up(queryRunner: QueryRunner): Promise { + // For each row in series_metadata set updatedAt to 0 + await queryRunner.query( + `UPDATE series_metadata SET "updatedAt" = '1980-01-01T00:00:00.000Z'`, + ); + // await queryRunner.query(`DELETE FROM series_metadata`); + } + + public async down(queryRunner: QueryRunner): Promise { + // Addition is backwards compatible + } +} diff --git a/backend/package.json b/backend/package.json index a795488..ddd1432 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,7 +24,7 @@ "plugin-api:publish": "npm publish -w packages/reiverr-plugin", "openapi:generate:tmdb": "ts-node scripts/generate-tmdb-openapi.ts", "openapi:generate-spec": "ts-node scripts/generate-openapi-spec.ts", - "typeorm": "ts-node ./node_modules/typeorm/cli", + "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", diff --git a/backend/src/metadata/metadata.controller.ts b/backend/src/metadata/metadata.controller.ts index ac653b0..f1bf2bd 100644 --- a/backend/src/metadata/metadata.controller.ts +++ b/backend/src/metadata/metadata.controller.ts @@ -1,13 +1,47 @@ -import { Controller, Post, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + NotFoundException, + Param, + Post, + UseGuards, +} from '@nestjs/common'; import { UserAccessControl } from 'src/auth/auth.guard'; import { MetadataService } from './metadata.service'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { MovieMetadata, SeriesMetadata } from './metadata.entity'; @ApiTags('metadata') @Controller('metadata') export class MetadataController { constructor(private metadataService: MetadataService) {} + @UseGuards(UserAccessControl) + @Get('movie/:tmdbId') + @ApiOkResponse({ type: MovieMetadata }) + async getMovie(@Param('tmdbId') tmdbId: string): Promise { + const movie = await this.metadataService.getMovieByTmdbId(tmdbId, true); + + if (!movie) { + throw new NotFoundException(`Movie with tmdbId ${tmdbId} not found`); + } + + return movie; + } + + @UseGuards(UserAccessControl) + @Get('series/:tmdbId') + @ApiOkResponse({ type: SeriesMetadata }) + async getSeries(@Param('tmdbId') tmdbId: string): Promise { + const series = await this.metadataService.getSeriesByTmdbId(tmdbId, true); + + if (!series) { + throw new NotFoundException(`Series with tmdbId ${tmdbId} not found`); + } + + return series; + } + @UseGuards(UserAccessControl) @Post('clear-cache') async clearCache() { diff --git a/backend/src/metadata/metadata.dto.ts b/backend/src/metadata/metadata.dto.ts index 66f3e90..48025b7 100644 --- a/backend/src/metadata/metadata.dto.ts +++ b/backend/src/metadata/metadata.dto.ts @@ -6,8 +6,8 @@ import { MovieMetadata } from './metadata.entity'; // inLibrary: boolean; // } -export class MovieDto extends MovieMetadata { - // tmdbData: any; - // @ApiProperty({ type: MovieUserDataDto, required: false }) - // userData?: MovieUserDataDto; -} +// export class MovieDto extends MovieMetadata { +// // tmdbData: any; +// // @ApiProperty({ type: MovieUserDataDto, required: false }) +// // userData?: MovieUserDataDto; +// } diff --git a/backend/src/metadata/metadata.entity.ts b/backend/src/metadata/metadata.entity.ts index 67fefed..ecad38c 100644 --- a/backend/src/metadata/metadata.entity.ts +++ b/backend/src/metadata/metadata.entity.ts @@ -26,7 +26,7 @@ export class MovieMetadata { // - @ApiProperty({ required: false, type: 'object' }) + @ApiProperty({ required: false, type: TmdbMovieFull }) @Column('json') tmdbMovie: TmdbMovieFull; @@ -48,10 +48,24 @@ export class MovieMetadata { @UpdateDateColumn() updatedAt: Date; - /** - * Requires update before serving - */ - isOutdated() { + private constructor() {} + + static from(tmdbMovie: TmdbMovieFull) { + return new MovieMetadata().updateFrom(tmdbMovie); + } + + updateFrom(tmdbMovie: TmdbMovieFull): MovieMetadata { + this.tmdbId = String(tmdbMovie.id); + this.tmdbMovie = tmdbMovie; + this.updatedAt = new Date(); + this.name = tmdbMovie.title; + this.releaseDate = tmdbMovie.release_date + ? new Date(tmdbMovie.release_date) + : undefined; + return this; + } + + needsUpdate() { const releaseDate = this.tmdbMovie?.release_date; if (!this.tmdbMovie) return true; @@ -63,14 +77,7 @@ export class MovieMetadata { ) return true; - return false; - } - - /** - * Can be lazily updated after serving - */ - isStale() { - if (this.isOutdated()) return true; + // Is stale if (new Date().getTime() - this.updatedAt.getTime() > TMDB_CACHE_TTL) return true; @@ -91,7 +98,7 @@ export class SeriesMetadata { // - @ApiProperty({ required: false, type: 'object' }) + @ApiProperty({ required: false, type: TmdbSeriesFull }) @Column('json') tmdbSeries: TmdbSeriesFull; @@ -129,12 +136,36 @@ export class SeriesMetadata { @UpdateDateColumn() updatedAt: Date; - /** - * Requires update before serving - */ - isOutdated() { + private constructor() {} + + static from(tmdbSeries: TmdbSeriesFull) { + return new SeriesMetadata().updateFrom(tmdbSeries); + } + + updateFrom(tmdbSeries: TmdbSeriesFull): SeriesMetadata { + this.tmdbId = String(tmdbSeries.id); + this.tmdbSeries = tmdbSeries; + this.updatedAt = new Date(); + this.firstReleaseDate = tmdbSeries.first_air_date + ? new Date(tmdbSeries.first_air_date) + : undefined; + this.lastReleaseDate = tmdbSeries.last_air_date + ? new Date(tmdbSeries.last_air_date) + : undefined; + this.nextReleaseDate = tmdbSeries.next_episode_to_air?.air_date + ? new Date(tmdbSeries.next_episode_to_air.air_date) + : undefined; + this.lastEpisodeNumber = tmdbSeries.last_episode_to_air?.episode_number; + this.lastSeasonNumber = tmdbSeries.last_episode_to_air?.season_number; + this.name = tmdbSeries.name; + return this; + } + + needsUpdate() { const nextAirDate = this.tmdbSeries?.next_episode_to_air?.air_date; + // Is missing an episode + if (!this.tmdbSeries) return true; if (!this.updatedAt) return true; if ( @@ -144,14 +175,7 @@ export class SeriesMetadata { ) return true; - return false; - } - - /** - * Can be lazily updated after serving - */ - isStale() { - if (this.isOutdated()) return true; + // Is stale if (new Date().getTime() - this.updatedAt.getTime() > TMDB_CACHE_TTL) return true; diff --git a/backend/src/metadata/metadata.service.ts b/backend/src/metadata/metadata.service.ts index 8392ab9..d825622 100644 --- a/backend/src/metadata/metadata.service.ts +++ b/backend/src/metadata/metadata.service.ts @@ -7,11 +7,12 @@ import { } from './metadata.entity'; import { MOVIE_REPOSITORY, SERIES_REPOSITORY } from './metadata.providers'; import { TmdbService } from './tmdb/tmdb.service'; -import { TmdbEpisodeFull } from './tmdb/tmdb.dto'; @Injectable() export class MetadataService { private logger = new Logger(MetadataService.name); + // (TODO: Should use db locks instead of in-memory locks for multiple instance support) + private updateLocks = new MetadataUpdateLock(); constructor( @Inject(MOVIE_REPOSITORY) @@ -28,86 +29,125 @@ export class MetadataService { await this.seriesRepository.clear(); } - async getMovieByTmdbId(tmdbId: string): Promise { + async getMovieByTmdbId( + tmdbId: string, + eager = false, + ): Promise { + const unlock = await this.updateLocks.acquire(tmdbId); + let movie = await this.movieRepository.findOne({ where: { tmdbId } }); + const updatedAt = movie?.updatedAt; - if (!movie) { - movie = new MovieMetadata(); - movie.tmdbId = tmdbId; - } - - if (movie.isStale()) { - const updatedMovie = this.tmdbService + if (!movie || movie.needsUpdate()) { + this.logger.debug(`Caching movie ${tmdbId}`); + const p = this.tmdbService .getFullMovie(Number(tmdbId)) .then(async (tmdbMovie) => { + this.logger.debug(`Fetched movie data from TMDB for ${tmdbId}`); if (tmdbMovie) { - movie.tmdbMovie = tmdbMovie; - movie.updatedAt = new Date(); - movie.name = tmdbMovie.title; - movie.releaseDate = tmdbMovie.release_date - ? new Date(tmdbMovie.release_date) - : undefined; + if (!movie) { + movie = MovieMetadata.from(tmdbMovie); + await this.movieRepository + .insert(movie) + .catch((e) => + this.logger.error( + `Failed to insert movie metadata for tmdbId ${tmdbId}`, + e.stack, + ), + ); + } else { + movie.updateFrom(tmdbMovie); + await this.movieRepository + .save(movie) + .catch((e) => + this.logger.error( + `Failed to update movie metadata for tmdbId ${tmdbId}`, + e.stack, + ), + ); + } + } else { + this.logger.warn( + `TMDB returned no data for movie with tmdbId ${tmdbId}`, + ); } - await this.movieRepository.upsert(movie, { - conflictPaths: ['tmdbId'], - }); + if (!movie) { + throw new Error( + `Failed to fetch metadata for movie with tmdbId ${tmdbId}`, + ); + } return movie; }); - if (movie.isOutdated()) return updatedMovie; + if (!movie || eager) { + await p; + } } + unlock(); return movie; } - async getBulkMoviesByTmdbIds(tmdbIds: string[]): Promise { - return []; - } + async getSeriesByTmdbId( + tmdbId: string, + eager = false, + ): Promise { + const unlock = await this.updateLocks.acquire(tmdbId); - async getSeriesByTmdbId(tmdbId: string): Promise { let series = await this.seriesRepository.findOne({ where: { tmdbId } }); + const updatedAt = series?.updatedAt; - if (!series) { - series = new SeriesMetadata(); - series.tmdbId = tmdbId; - } - - if (series.isStale()) { + if (!series || series.needsUpdate()) { this.logger.debug(`Caching series ${tmdbId}`); - const updatedSeries = this.tmdbService - .getFullSeries(Number(tmdbId)) + const p = this.tmdbService + .getFullSeries(tmdbId) .then(async (tmdbSeries) => { + this.logger.debug(`Fetched series data from TMDB for ${tmdbId}`); if (tmdbSeries) { - series.tmdbSeries = tmdbSeries; - series.updatedAt = new Date(); - series.firstReleaseDate = tmdbSeries.first_air_date - ? new Date(tmdbSeries.first_air_date) - : undefined; - series.lastReleaseDate = tmdbSeries.last_air_date - ? new Date(tmdbSeries.last_air_date) - : undefined; - series.nextReleaseDate = tmdbSeries.next_episode_to_air?.air_date - ? new Date(tmdbSeries.next_episode_to_air.air_date) - : undefined; - series.lastEpisodeNumber = - tmdbSeries.last_episode_to_air?.episode_number; - series.lastSeasonNumber = - tmdbSeries.last_episode_to_air?.season_number; - series.name = tmdbSeries.name; + if (!series) { + series = SeriesMetadata.from(tmdbSeries); + await this.seriesRepository + .insert(series) + .catch((e) => + this.logger.error( + `Failed to insert series metadata for tmdbId ${tmdbId}`, + e.stack, + ), + ); + } else { + series.updateFrom(tmdbSeries); + await this.seriesRepository + .save(series) + .catch((e) => + this.logger.error( + `Failed to update series metadata for tmdbId ${tmdbId}`, + e.stack, + ), + ); + } + } else { + this.logger.warn( + `TMDB returned no data for series with tmdbId ${tmdbId}`, + ); } - await this.seriesRepository.upsert(series, { - conflictPaths: ['tmdbId'], - }); + if (!series) { + throw new Error( + `Failed to fetch metadata for series with tmdbId ${tmdbId}`, + ); + } return series; }); - if (series.isOutdated()) return updatedSeries; + if (!series || eager) { + await p; + } } + unlock(); return series; } @@ -125,4 +165,30 @@ export class MetadataService { tmdbEpisode, }; } + + async getBulkMoviesByTmdbIds(tmdbIds: string[]): Promise { + return []; + } +} + +class MetadataUpdateLock { + private locks = new Map>(); + + async acquire(key: string) { + if (this.locks.has(key)) { + await this.locks.get(key); + } + + let resolveFn: (value: any) => void; + const promise = new Promise(async (resolve) => { + resolveFn = resolve; + }); + + this.locks.set(key, promise); + + return () => { + resolveFn(null); + this.locks.delete(key); + }; + } } diff --git a/backend/src/metadata/tmdb/tmdb.dto.ts b/backend/src/metadata/tmdb/tmdb.dto.ts index 3af6621..96080fd 100644 --- a/backend/src/metadata/tmdb/tmdb.dto.ts +++ b/backend/src/metadata/tmdb/tmdb.dto.ts @@ -1,5 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; import { TmdbApi } from './tmdb.providers'; +import { + MovieCreditsDto, + MovieDetailsDto, + MovieExternalIdsDto, + MovieImagesDto, + MovieVideosDto, + SeasonDto, + TvSeasonDetailsDto, + TvSeriesAggregateCreditsDto, + TvSeriesDetailsDto, + TvSeriesExternalIdsDto, + TvSeriesImagesDto, + TvSeriesVideosDto, +} from './tmdb.v3.generated.dto'; export type MovieVideos = Awaited< ReturnType @@ -37,54 +51,93 @@ export type TmdbEpisode = Awaited< ReturnType >['data']; -export type TmdbMovieFull = TmdbMovie & { - videos: MovieVideos; // Proxy or to not proxy - credits: MovieCredits; - external_ids: MovieExternalIds; - images: MovieImages; -}; +// export type TmdbMovieFull = TmdbMovie & { +// videos: MovieVideos; // Proxy or to not proxy +// credits: MovieCredits; +// external_ids: MovieExternalIds; +// images: MovieImages; +// }; -export type TmdbSeriesFull = TmdbSeries & { - videos: SeriesVideos; - aggregate_credits: SeriesCredits; - external_ids: SeriesExternalIds; - images: SeriesImages; -}; +export class TmdbMovieFull extends MovieDetailsDto { + @ApiProperty({ required: false }) + videos?: MovieVideosDto; -export type TmdbEpisodeFull = TmdbEpisode; + @ApiProperty({ required: false }) + credits?: MovieCreditsDto; + + @ApiProperty({ required: false }) + external_ids?: MovieExternalIdsDto; + + @ApiProperty({ required: false }) + images?: MovieImagesDto; +} + +// export type TmdbSeriesFull = TmdbSeries & { +// videos: SeriesVideos; +// aggregate_credits: SeriesCredits; +// external_ids: SeriesExternalIds; +// images: SeriesImages; +// }; class NextEpisodeToAir { @ApiProperty({ required: false }) air_date?: string; } -class Season { +export class TmdbSeasonFull extends TvSeasonDetailsDto { @ApiProperty({ required: false }) - air_date?: string; - - @ApiProperty({ required: false }) - episode_count?: number; - - @ApiProperty({ required: false }) - id?: number; - - @ApiProperty({ required: false }) - name?: string; - - @ApiProperty({ required: false }) - overview?: string; - - @ApiProperty({ required: false }) - poster_path?: string; - - @ApiProperty({ required: false }) - season_number?: number; - - @ApiProperty({ required: false }) - vote_average?: number; + aggregate_credits?: TvSeriesAggregateCreditsDto; } -export class TmdbItemDto implements TmdbMovie, TmdbSeries { +export class TmdbSeriesFull extends TvSeriesDetailsDto { + @ApiProperty({ required: false }) + videos?: TvSeriesVideosDto; + + @ApiProperty({ required: false }) + aggregate_credits?: TvSeriesAggregateCreditsDto; + + @ApiProperty({ required: false }) + external_ids?: TvSeriesExternalIdsDto; + + @ApiProperty({ required: false }) + images?: TvSeriesImagesDto; + + @ApiProperty({ required: false, type: NextEpisodeToAir }) + next_episode_to_air?: NextEpisodeToAir; + + @ApiProperty({ required: false, type: [TmdbSeasonFull] }) + seasons?: TmdbSeasonFull[]; +} + +export type TmdbEpisodeFull = TmdbEpisode; + +// class Season { +// @ApiProperty({ required: false }) +// air_date?: string; + +// @ApiProperty({ required: false }) +// episode_count?: number; + +// @ApiProperty({ required: false }) +// id?: number; + +// @ApiProperty({ required: false }) +// name?: string; + +// @ApiProperty({ required: false }) +// overview?: string; + +// @ApiProperty({ required: false }) +// poster_path?: string; + +// @ApiProperty({ required: false }) +// season_number?: number; + +// @ApiProperty({ required: false }) +// vote_average?: number; +// } + +export class TmdbItemDto implements MovieDetailsDto, TvSeriesDetailsDto { // TmdbMovie & TmdbSeries @ApiProperty({ required: false }) @@ -96,7 +149,7 @@ export class TmdbItemDto implements TmdbMovie, TmdbSeries { @ApiProperty({ required: false }) vote_average?: number; - // TmdbMovie only + // TmdbMovie only, therefore optional @ApiProperty({ required: false }) title?: string; @@ -107,7 +160,7 @@ export class TmdbItemDto implements TmdbMovie, TmdbSeries { @ApiProperty({ required: false }) runtime?: number; - // TmdbSeries only + // TmdbSeries only, therefore optional @ApiProperty({ required: false }) name?: string; @@ -121,6 +174,6 @@ export class TmdbItemDto implements TmdbMovie, TmdbSeries { @ApiProperty({ required: false, type: NextEpisodeToAir }) next_episode_to_air?: NextEpisodeToAir; - @ApiProperty({ required: false, isArray: true, type: Season }) - seasons?: Season[]; + @ApiProperty({ required: false, type: [SeasonDto] }) + seasons?: SeasonDto[]; } diff --git a/backend/src/metadata/tmdb/tmdb.service.ts b/backend/src/metadata/tmdb/tmdb.service.ts index ac01bea..9429e46 100644 --- a/backend/src/metadata/tmdb/tmdb.service.ts +++ b/backend/src/metadata/tmdb/tmdb.service.ts @@ -1,6 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; -import { TmdbEpisodeFull, TmdbMovieFull, TmdbSeriesFull } from './tmdb.dto'; +import { + TmdbEpisodeFull, + TmdbMovieFull, + TmdbSeasonFull, + TmdbSeriesFull, +} from './tmdb.dto'; import { TMDB_API, TmdbApi } from './tmdb.providers'; +import { TvSeasonDetailsDto } from './tmdb.v3.generated.dto'; @Injectable() export class TmdbService { @@ -9,7 +15,15 @@ export class TmdbService { private tmdbApi: TmdbApi, ) {} - async getFullSeries(tmdbId: number): Promise { + 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); + } + + async getFullSeries(tmdbId: string): Promise { const tmdbSeries = await this.tmdbApi.v3 .tvSeriesDetails(Number(tmdbId), { append_to_response: 'videos,aggregate_credits,external_ids,images', @@ -20,15 +34,29 @@ export class TmdbService { // return e; // }); + const seasons = tmdbSeries?.seasons + ?.filter( + (s) => + !s.name?.toLocaleLowerCase()?.includes('special') && + s.season_number !== 0, + ) + ?.map((season) => + this.getFullSeason({ tmdbId, season: season.season_number! }), + ); + tmdbSeries.seasons = await Promise.all(seasons); + return tmdbSeries; } - async getFullMovie(tmdbId: number) { + async getFullSeason(options: { + tmdbId: string; + season: number; + }): Promise { return this.tmdbApi.v3 - .movieDetails(Number(tmdbId), { - append_to_response: 'videos,credits,external_ids,images', + .tvSeasonDetails(Number(options.tmdbId), options.season, { + append_to_response: 'aggregate_credits', }) - .then((r) => r.data as TmdbMovieFull); + .then((r) => r.data as TmdbSeasonFull); } async getFullEpisode(options: { diff --git a/backend/src/metadata/tmdb/tmdb.v3.generated.dto.ts b/backend/src/metadata/tmdb/tmdb.v3.generated.dto.ts new file mode 100644 index 0000000..c85bf0c --- /dev/null +++ b/backend/src/metadata/tmdb/tmdb.v3.generated.dto.ts @@ -0,0 +1,1629 @@ +/* eslint-disable */ +/** + * ===================================================================== + * THIS FILE WAS AI-GENERATED + * ===================================================================== + * + * These types were automatically generated from the TMDB v3 OpenAPI + * specification (tmdb.v3.openapi.ts) and converted to NestJS DTOs with + * Swagger decorators for API documentation. + * + * Generation prompt: + * "Can you create tmdb.v3.generated.dto.ts and in there create nestjs swagger + * classes for all types from tmdb.v3.openapi.ts. You can look at + * source-provider.dto.ts or media-source.dto.ts for reference if you need it. + * At the top of the new file include a disclaimer that the types were AI + * generated and include this prompt word-to-word." + * + * ===================================================================== + */ + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +// ===================================================================== +// COMMON/SHARED TYPES +// ===================================================================== + +export class GenreDto { + @ApiPropertyOptional({ example: 18, description: 'Genre ID' }) + id?: number; + + @ApiPropertyOptional({ example: 'Drama', description: 'Genre name' }) + name?: string; +} + +export class ProductionCompanyDto { + @ApiPropertyOptional({ example: 508, description: 'Company ID' }) + id?: number; + + @ApiPropertyOptional({ + example: '/7cxRWzi4LsVm4Utfpr1hfARNurT.png', + description: 'Company logo path', + }) + logo_path?: string; + + @ApiPropertyOptional({ + example: 'Regency Enterprises', + description: 'Company name', + }) + name?: string; + + @ApiPropertyOptional({ example: 'US', description: 'Origin country code' }) + origin_country?: string; +} + +export class ProductionCountryDto { + @ApiPropertyOptional({ + example: 'US', + description: 'Country code (ISO 3166-1)', + }) + iso_3166_1?: string; + + @ApiPropertyOptional({ + example: 'United States of America', + description: 'Country name', + }) + name?: string; +} + +export class SpokenLanguageDto { + @ApiPropertyOptional({ + example: 'English', + description: 'Language name in English', + }) + english_name?: string; + + @ApiPropertyOptional({ + example: 'en', + description: 'Language code (ISO 639-1)', + }) + iso_639_1?: string; + + @ApiPropertyOptional({ + example: 'English', + description: 'Language native name', + }) + name?: string; +} + +export class NetworkDto { + @ApiPropertyOptional({ example: 49, description: 'Network ID' }) + id?: number; + + @ApiPropertyOptional({ + example: '/tuomPhY2UtuPTqqFnKMVHvSb724.png', + description: 'Network logo path', + }) + logo_path?: string; + + @ApiPropertyOptional({ example: 'HBO', description: 'Network name' }) + name?: string; + + @ApiPropertyOptional({ example: 'US', description: 'Origin country code' }) + origin_country?: string; +} + +export class CreatorDto { + @ApiPropertyOptional({ example: 9813, description: 'Creator ID' }) + id?: number; + + @ApiPropertyOptional({ + example: '5256c8c219c2956ff604858a', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ + example: 'David Benioff', + description: 'Creator name', + }) + name?: string; + + @ApiPropertyOptional({ + example: 2, + description: 'Gender (0=not specified, 1=female, 2=male)', + }) + gender?: number; + + @ApiPropertyOptional({ + example: '/xvNN5huL0X8yJ7h3IZfGG4O2zBD.jpg', + description: 'Profile image path', + }) + profile_path?: string; +} + +// ===================================================================== +// MOVIE TYPES +// ===================================================================== + +export class MovieDetailsDto { + @ApiPropertyOptional({ + example: false, + description: 'Whether the movie is adult content', + }) + adult?: boolean; + + @ApiPropertyOptional({ + example: '/hZkgoQYus5vegHoetLkCJzb17zJ.jpg', + description: 'Path to backdrop image', + }) + backdrop_path?: string; + + @ApiPropertyOptional({ + description: 'Collection information if part of a collection', + }) + belongs_to_collection?: any; + + @ApiPropertyOptional({ + example: 63000000, + description: 'Movie production budget', + }) + budget?: number; + + @ApiPropertyOptional({ + type: [GenreDto], + description: 'Array of genres', + }) + genres?: GenreDto[]; + + @ApiPropertyOptional({ + example: 'http://www.foxmovies.com/movies/fight-club', + description: 'Official movie website', + }) + homepage?: string; + + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'tt0137523', + description: 'IMDb identifier', + }) + imdb_id?: string; + + @ApiPropertyOptional({ + example: 'en', + description: 'Original language code (ISO 639-1)', + }) + original_language?: string; + + @ApiPropertyOptional({ + example: 'Fight Club', + description: 'Original movie title', + }) + original_title?: string; + + @ApiPropertyOptional({ + example: + 'A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy.', + description: 'Movie plot summary', + }) + overview?: string; + + @ApiPropertyOptional({ example: 61.416, description: 'Popularity score' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg', + description: 'Path to poster image', + }) + poster_path?: string; + + @ApiPropertyOptional({ + type: [ProductionCompanyDto], + description: 'Array of production companies', + }) + production_companies?: ProductionCompanyDto[]; + + @ApiPropertyOptional({ + type: [ProductionCountryDto], + description: 'Array of production countries', + }) + production_countries?: ProductionCountryDto[]; + + @ApiPropertyOptional({ + example: '1999-10-15', + description: 'Release date (YYYY-MM-DD)', + }) + release_date?: string; + + @ApiPropertyOptional({ + example: 100853753, + description: 'Box office revenue', + }) + revenue?: number; + + @ApiPropertyOptional({ example: 139, description: 'Duration in minutes' }) + runtime?: number; + + @ApiPropertyOptional({ + type: [SpokenLanguageDto], + description: 'Array of spoken languages', + }) + spoken_languages?: SpokenLanguageDto[]; + + @ApiPropertyOptional({ example: 'Released', description: 'Release status' }) + status?: string; + + @ApiPropertyOptional({ + example: 'Mischief. Mayhem. Soap.', + description: 'Movie tagline', + }) + tagline?: string; + + @ApiPropertyOptional({ example: 'Fight Club', description: 'Movie title' }) + title?: string; + + @ApiPropertyOptional({ + example: false, + description: 'Whether video is available', + }) + video?: boolean; + + @ApiPropertyOptional({ example: 8.433, description: 'Average rating' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 26280, description: 'Number of votes' }) + vote_count?: number; +} + +export class MovieSearchResultItemDto { + @ApiPropertyOptional({ example: false, description: 'Whether adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ + example: '/hZkgoQYus5vegHoetLkCJzb17zJ.jpg', + description: 'Backdrop image path', + }) + backdrop_path?: string; + + @ApiPropertyOptional({ + type: [Number], + example: [18, 53, 28], + description: 'Array of genre IDs', + }) + genre_ids?: number[]; + + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ example: 'en', description: 'Original language code' }) + original_language?: string; + + @ApiPropertyOptional({ example: 'Fight Club', description: 'Original title' }) + original_title?: string; + + @ApiPropertyOptional({ + example: 'A ticking-time-bomb insomniac and a slippery soap salesman...', + description: 'Overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: 73.433, description: 'Popularity score' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ + example: '1999-10-15', + description: 'Release date', + }) + release_date?: string; + + @ApiPropertyOptional({ example: 'Fight Club', description: 'Movie title' }) + title?: string; + + @ApiPropertyOptional({ example: false, description: 'Has video' }) + video?: boolean; + + @ApiPropertyOptional({ example: 8.433, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 26279, description: 'Vote count' }) + vote_count?: number; +} + +export class MovieSearchResponseDto { + @ApiPropertyOptional({ example: 1, description: 'Current page number' }) + page?: number; + + @ApiPropertyOptional({ + type: [MovieSearchResultItemDto], + description: 'Array of movie results', + }) + results?: MovieSearchResultItemDto[]; + + @ApiPropertyOptional({ example: 2, description: 'Total pages available' }) + total_pages?: number; + + @ApiPropertyOptional({ example: 39, description: 'Total results count' }) + total_results?: number; +} + +// ===================================================================== +// TV SERIES TYPES +// ===================================================================== + +export class EpisodeToAirDto { + @ApiPropertyOptional({ example: 1551830, description: 'Episode ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'The Iron Throne', + description: 'Episode name', + }) + name?: string; + + @ApiPropertyOptional({ + example: 'In the aftermath of the devastating attack...', + description: 'Episode overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: 4.809, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 241, description: 'Vote count' }) + vote_count?: number; + + @ApiPropertyOptional({ + example: '2019-05-19', + description: 'Air date (YYYY-MM-DD)', + }) + air_date?: string; + + @ApiPropertyOptional({ example: 6, description: 'Episode number' }) + episode_number?: number; + + @ApiPropertyOptional({ example: '806', description: 'Production code' }) + production_code?: string; + + @ApiPropertyOptional({ example: 80, description: 'Runtime in minutes' }) + runtime?: number; + + @ApiPropertyOptional({ example: 8, description: 'Season number' }) + season_number?: number; + + @ApiPropertyOptional({ example: 1399, description: 'Show ID' }) + show_id?: number; + + @ApiPropertyOptional({ + example: '/zBi2O5EJfgTS6Ae0HdAYLm9o2nf.jpg', + description: 'Still image path', + }) + still_path?: string; +} + +export class SeasonDto { + @ApiPropertyOptional({ + example: '2010-12-05', + description: 'Season air date', + }) + air_date?: string; + + @ApiPropertyOptional({ example: 272, description: 'Episode count' }) + episode_count?: number; + + @ApiPropertyOptional({ example: 3627, description: 'Season ID' }) + id?: number; + + @ApiPropertyOptional({ example: 'Specials', description: 'Season name' }) + name?: string; + + @ApiPropertyOptional({ example: '', description: 'Season overview' }) + overview?: string; + + @ApiPropertyOptional({ + example: '/kMTcwNRfFKCZ0O2OaBZS0nZ2AIe.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ example: 0, description: 'Season number' }) + season_number?: number; + + @ApiPropertyOptional({ example: 0, description: 'Vote average' }) + vote_average?: number; +} + +export class TvSeriesDetailsDto { + @ApiPropertyOptional({ example: false, description: 'Whether adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ + example: '/6LWy0jvMpmjoS9fojNgHIKoWL05.jpg', + description: 'Backdrop path', + }) + backdrop_path?: string; + + @ApiPropertyOptional({ + type: [CreatorDto], + description: 'Array of series creators', + }) + created_by?: CreatorDto[]; + + @ApiPropertyOptional({ + type: [Number], + example: [60, 45], + description: 'Array of typical episode durations', + }) + episode_run_time?: number[]; + + @ApiPropertyOptional({ + example: '2011-04-17', + description: 'First air date', + }) + first_air_date?: string; + + @ApiPropertyOptional({ + type: [GenreDto], + description: 'Array of genres', + }) + genres?: GenreDto[]; + + @ApiPropertyOptional({ + example: 'http://www.hbo.com/game-of-thrones', + description: 'Official homepage', + }) + homepage?: string; + + @ApiPropertyOptional({ example: 1399, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ + example: false, + description: 'Whether still in production', + }) + in_production?: boolean; + + @ApiPropertyOptional({ + type: [String], + example: ['en', 'es'], + description: 'Array of language codes', + }) + languages?: string[]; + + @ApiPropertyOptional({ + example: '2019-05-19', + description: 'Most recent air date', + }) + last_air_date?: string; + + @ApiPropertyOptional({ + type: EpisodeToAirDto, + description: 'Last episode to air', + }) + last_episode_to_air?: EpisodeToAirDto; + + @ApiPropertyOptional({ + example: 'Game of Thrones', + description: 'Series name', + }) + name?: string; + + @ApiPropertyOptional({ description: 'Next episode to air (if any)' }) + next_episode_to_air?: any; + + @ApiPropertyOptional({ + type: [NetworkDto], + description: 'Array of networks', + }) + networks?: NetworkDto[]; + + @ApiPropertyOptional({ example: 73, description: 'Total number of episodes' }) + number_of_episodes?: number; + + @ApiPropertyOptional({ example: 8, description: 'Total number of seasons' }) + number_of_seasons?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['US', 'GB'], + description: 'Array of origin countries', + }) + origin_country?: string[]; + + @ApiPropertyOptional({ + example: 'en', + description: 'Original language code', + }) + original_language?: string; + + @ApiPropertyOptional({ + example: 'Game of Thrones', + description: 'Original series name', + }) + original_name?: string; + + @ApiPropertyOptional({ + example: + 'Seven noble families fight for control of the mythical land of Westeros...', + description: 'Series description', + }) + overview?: string; + + @ApiPropertyOptional({ example: 346.098, description: 'Popularity score' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/1XS1oqL89opfnbLl8WnZY1O1uJx.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ + type: [ProductionCompanyDto], + description: 'Array of production companies', + }) + production_companies?: ProductionCompanyDto[]; + + @ApiPropertyOptional({ + type: [ProductionCountryDto], + description: 'Array of production countries', + }) + production_countries?: ProductionCountryDto[]; + + @ApiPropertyOptional({ + type: [SeasonDto], + description: 'Array of seasons', + }) + seasons?: SeasonDto[]; + + @ApiPropertyOptional({ + type: [SpokenLanguageDto], + description: 'Array of spoken languages', + }) + spoken_languages?: SpokenLanguageDto[]; + + @ApiPropertyOptional({ example: 'Ended', description: 'Series status' }) + status?: string; + + @ApiPropertyOptional({ + example: 'Winter Is Coming', + description: 'Series tagline', + }) + tagline?: string; + + @ApiPropertyOptional({ example: 'Scripted', description: 'Series type' }) + type?: string; + + @ApiPropertyOptional({ example: 8.438, description: 'Average rating' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 21390, description: 'Vote count' }) + vote_count?: number; +} + +export class TvSearchResultItemDto { + @ApiPropertyOptional({ example: false, description: 'Whether adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ + example: '/bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg', + description: 'Backdrop path', + }) + backdrop_path?: string; + + @ApiPropertyOptional({ + type: [Number], + example: [18, 80], + description: 'Array of genre IDs', + }) + genre_ids?: number[]; + + @ApiPropertyOptional({ example: 1396, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['US'], + description: 'Origin countries', + }) + origin_country?: string[]; + + @ApiPropertyOptional({ example: 'en', description: 'Original language' }) + original_language?: string; + + @ApiPropertyOptional({ + example: 'Breaking Bad', + description: 'Original name', + }) + original_name?: string; + + @ApiPropertyOptional({ + example: 'When Walter White, a New Mexico chemistry teacher...', + description: 'Overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: 298.884, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/ggFHVNu6YYI5L9pCfOacjizRGt.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ + example: '2008-01-20', + description: 'First air date', + }) + first_air_date?: string; + + @ApiPropertyOptional({ example: 'Breaking Bad', description: 'Series name' }) + name?: string; + + @ApiPropertyOptional({ example: 8.879, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 11536, description: 'Vote count' }) + vote_count?: number; +} + +export class TvSearchResponseDto { + @ApiPropertyOptional({ example: 1, description: 'Current page' }) + page?: number; + + @ApiPropertyOptional({ + type: [TvSearchResultItemDto], + description: 'Search results', + }) + results?: TvSearchResultItemDto[]; + + @ApiPropertyOptional({ example: 1, description: 'Total pages' }) + total_pages?: number; + + @ApiPropertyOptional({ example: 1, description: 'Total results' }) + total_results?: number; +} + +// ===================================================================== +// EPISODE TYPES +// ===================================================================== + +export class CrewMemberDto { + @ApiPropertyOptional({ example: 'Directing', description: 'Department' }) + department?: string; + + @ApiPropertyOptional({ example: 'Director', description: 'Job title' }) + job?: string; + + @ApiPropertyOptional({ + example: '5256c8a219c2956ff6046e77', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ example: 2, description: 'Gender' }) + gender?: number; + + @ApiPropertyOptional({ example: 44797, description: 'Person ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Directing', + description: 'Known for department', + }) + known_for_department?: string; + + @ApiPropertyOptional({ example: 'Timothy Van Patten', description: 'Name' }) + name?: string; + + @ApiPropertyOptional({ + example: 'Timothy Van Patten', + description: 'Original name', + }) + original_name?: string; + + @ApiPropertyOptional({ example: 7.775, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/MzSOFrd99HRdr6pkSRSctk3kBR.jpg', + description: 'Profile path', + }) + profile_path?: string; +} + +export class GuestStarDto { + @ApiPropertyOptional({ + example: 'Benjen Stark', + description: 'Character name', + }) + character?: string; + + @ApiPropertyOptional({ + example: '5256c8b919c2956ff604836a', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ example: 62, description: 'Billing order' }) + order?: number; + + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ example: 2, description: 'Gender' }) + gender?: number; + + @ApiPropertyOptional({ example: 119783, description: 'Person ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Acting', + description: 'Known for department', + }) + known_for_department?: string; + + @ApiPropertyOptional({ example: 'Joseph Mawle', description: 'Name' }) + name?: string; + + @ApiPropertyOptional({ + example: 'Joseph Mawle', + description: 'Original name', + }) + original_name?: string; + + @ApiPropertyOptional({ example: 6.758, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/1Ocb9v3h54beGVoJMm4w50UQhLf.jpg', + description: 'Profile path', + }) + profile_path?: string; +} + +export class TvEpisodeDetailsDto { + @ApiPropertyOptional({ + example: '2011-04-17', + description: 'Episode air date', + }) + air_date?: string; + + @ApiPropertyOptional({ + type: [CrewMemberDto], + description: 'Crew members', + }) + crew?: CrewMemberDto[]; + + @ApiPropertyOptional({ example: 1, description: 'Episode number' }) + episode_number?: number; + + @ApiPropertyOptional({ + type: [GuestStarDto], + description: 'Guest stars', + }) + guest_stars?: GuestStarDto[]; + + @ApiPropertyOptional({ + example: 'Winter Is Coming', + description: 'Episode name', + }) + name?: string; + + @ApiPropertyOptional({ + example: 'Jon Arryn, the Hand of the King, is dead...', + description: 'Episode overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: 63056, description: 'Episode ID' }) + id?: number; + + @ApiPropertyOptional({ example: '101', description: 'Production code' }) + production_code?: string; + + @ApiPropertyOptional({ example: 62, description: 'Runtime in minutes' }) + runtime?: number; + + @ApiPropertyOptional({ example: 1, description: 'Season number' }) + season_number?: number; + + @ApiPropertyOptional({ + example: '/9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg', + description: 'Still image path', + }) + still_path?: string; + + @ApiPropertyOptional({ example: 7.838, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 291, description: 'Vote count' }) + vote_count?: number; +} + +// ===================================================================== +// SEASON TYPES +// ===================================================================== + +export class SeasonEpisodeDto { + @ApiPropertyOptional({ + example: '2011-04-17', + description: 'Episode air date', + }) + air_date?: string; + + @ApiPropertyOptional({ example: 1, description: 'Episode number' }) + episode_number?: number; + + @ApiPropertyOptional({ example: 63056, description: 'Episode ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Winter Is Coming', + description: 'Episode name', + }) + name?: string; + + @ApiPropertyOptional({ + example: 'Jon Arryn, the Hand of the King, is dead...', + description: 'Overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: '101', description: 'Production code' }) + production_code?: string; + + @ApiPropertyOptional({ example: 62, description: 'Runtime in minutes' }) + runtime?: number; + + @ApiPropertyOptional({ example: 1, description: 'Season number' }) + season_number?: number; + + @ApiPropertyOptional({ example: 1399, description: 'Show ID' }) + show_id?: number; + + @ApiPropertyOptional({ + example: '/9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg', + description: 'Still path', + }) + still_path?: string; + + @ApiPropertyOptional({ example: 7.838, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 291, description: 'Vote count' }) + vote_count?: number; + + @ApiPropertyOptional({ + type: [CrewMemberDto], + description: 'Crew members', + }) + crew?: CrewMemberDto[]; + + @ApiPropertyOptional({ + type: [GuestStarDto], + description: 'Guest stars', + }) + guest_stars?: GuestStarDto[]; +} + +export class TvSeasonDetailsDto { + @ApiPropertyOptional({ + example: '5256c89f19c2956ff6046d47', + description: 'Internal ID', + }) + _id?: string; + + @ApiPropertyOptional({ + example: '2011-04-17', + description: 'Season air date', + }) + air_date?: string; + + @ApiPropertyOptional({ + type: [SeasonEpisodeDto], + description: 'Episodes in season', + }) + episodes?: SeasonEpisodeDto[]; + + @ApiPropertyOptional({ example: 'Season 1', description: 'Season name' }) + name?: string; + + @ApiPropertyOptional({ + example: 'Trouble is brewing in the Seven Kingdoms of Westeros...', + description: 'Season overview', + }) + overview?: string; + + @ApiPropertyOptional({ example: 3624, description: 'Season ID' }) + id?: number; + + @ApiPropertyOptional({ + example: '/wgfKiqzuMrFIkU1M68DDDY8kGC1.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ example: 1, description: 'Season number' }) + season_number?: number; + + @ApiPropertyOptional({ example: 8.3, description: 'Vote average' }) + vote_average?: number; +} + +// ===================================================================== +// CREDITS TYPES +// ===================================================================== + +export class CastMemberDto { + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ example: 2, description: 'Gender' }) + gender?: number; + + @ApiPropertyOptional({ example: 819, description: 'Actor ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Acting', + description: 'Known for department', + }) + known_for_department?: string; + + @ApiPropertyOptional({ example: 'Edward Norton', description: 'Actor name' }) + name?: string; + + @ApiPropertyOptional({ + example: 'Edward Norton', + description: 'Original name', + }) + original_name?: string; + + @ApiPropertyOptional({ example: 26.99, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/5XBzD5WuTyVQZeS4VI25z2moMeY.jpg', + description: 'Profile path', + }) + profile_path?: string; + + @ApiPropertyOptional({ example: 4, description: 'Cast ID' }) + cast_id?: number; + + @ApiPropertyOptional({ + example: 'The Narrator', + description: 'Character name', + }) + character?: string; + + @ApiPropertyOptional({ + example: '52fe4250c3a36847f800068f', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ example: 0, description: 'Billing order' }) + order?: number; +} + +export class MovieCreditsDto { + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [CastMemberDto], + description: 'Cast members', + }) + cast?: CastMemberDto[]; + + @ApiPropertyOptional({ + type: [CrewMemberDto], + description: 'Crew members', + }) + crew?: CrewMemberDto[]; +} + +export class AggregateRoleDto { + @ApiPropertyOptional({ + example: '52542282760ee313280017f9', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ + example: 'Eddard Stark', + description: 'Character name', + }) + character?: string; + + @ApiPropertyOptional({ example: 10, description: 'Episode count' }) + episode_count?: number; +} + +export class AggregateCastMemberDto { + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ example: 2, description: 'Gender' }) + gender?: number; + + @ApiPropertyOptional({ example: 239019, description: 'Actor ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Acting', + description: 'Known for department', + }) + known_for_department?: string; + + @ApiPropertyOptional({ example: 'Sean Bean', description: 'Actor name' }) + name?: string; + + @ApiPropertyOptional({ example: 'Sean Bean', description: 'Original name' }) + original_name?: string; + + @ApiPropertyOptional({ example: 20.991, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/kTjiABk3TJ3yI0Cto5RsvyT6V3o.jpg', + description: 'Profile path', + }) + profile_path?: string; + + @ApiPropertyOptional({ + type: [AggregateRoleDto], + description: 'Roles played', + }) + roles?: AggregateRoleDto[]; + + @ApiPropertyOptional({ example: 9, description: 'Total episode count' }) + total_episode_count?: number; + + @ApiPropertyOptional({ example: 0, description: 'Billing order' }) + order?: number; +} + +export class AggregateJobDto { + @ApiPropertyOptional({ + example: '5256c8a019c2956ff6046e1b', + description: 'Credit ID', + }) + credit_id?: string; + + @ApiPropertyOptional({ example: 'Director', description: 'Job title' }) + job?: string; + + @ApiPropertyOptional({ example: 22, description: 'Episode count' }) + episode_count?: number; +} + +export class AggregateCrewMemberDto { + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ example: 2, description: 'Gender' }) + gender?: number; + + @ApiPropertyOptional({ example: 44797, description: 'Person ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Directing', + description: 'Known for department', + }) + known_for_department?: string; + + @ApiPropertyOptional({ example: 'Timothy Van Patten', description: 'Name' }) + name?: string; + + @ApiPropertyOptional({ + example: 'Timothy Van Patten', + description: 'Original name', + }) + original_name?: string; + + @ApiPropertyOptional({ example: 7.775, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '/MzSOFrd99HRdr6pkSRSctk3kBR.jpg', + description: 'Profile path', + }) + profile_path?: string; + + @ApiPropertyOptional({ + type: [AggregateJobDto], + description: 'Jobs performed', + }) + jobs?: AggregateJobDto[]; + + @ApiPropertyOptional({ example: 'Directing', description: 'Department' }) + department?: string; + + @ApiPropertyOptional({ example: 22, description: 'Total episode count' }) + total_episode_count?: number; +} + +export class TvSeriesAggregateCreditsDto { + @ApiPropertyOptional({ example: 1399, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [AggregateCastMemberDto], + description: 'Cast members', + }) + cast?: AggregateCastMemberDto[]; + + @ApiPropertyOptional({ + type: [AggregateCrewMemberDto], + description: 'Crew members', + }) + crew?: AggregateCrewMemberDto[]; +} + +// ===================================================================== +// VIDEOS TYPES +// ===================================================================== + +export class VideoDto { + @ApiPropertyOptional({ + example: 'en', + description: 'Language code (ISO 639-1)', + }) + iso_639_1?: string; + + @ApiPropertyOptional({ + example: 'US', + description: 'Country code (ISO 3166-1)', + }) + iso_3166_1?: string; + + @ApiPropertyOptional({ + example: 'Official Trailer', + description: 'Video title', + }) + name?: string; + + @ApiPropertyOptional({ + example: 'SUXWAEX2jlg', + description: 'Video key/ID (e.g., YouTube video ID)', + }) + key?: string; + + @ApiPropertyOptional({ + example: 'YouTube', + description: 'Video platform', + }) + site?: string; + + @ApiPropertyOptional({ + example: 1080, + description: 'Video resolution (e.g., 720, 1080)', + }) + size?: number; + + @ApiPropertyOptional({ + example: 'Trailer', + description: 'Video type', + enum: [ + 'Trailer', + 'Teaser', + 'Clip', + 'Featurette', + 'Behind the Scenes', + 'Bloopers', + ], + }) + type?: string; + + @ApiPropertyOptional({ + example: true, + description: 'Whether it is official content', + }) + official?: boolean; + + @ApiPropertyOptional({ + example: '2019-04-08T13:00:00.000Z', + description: 'Publication date (ISO format)', + }) + published_at?: string; + + @ApiPropertyOptional({ + example: '533ec654c3a36854480003eb', + description: 'Video ID', + }) + id?: string; +} + +export class MovieVideosDto { + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [VideoDto], + description: 'Array of videos', + }) + results?: VideoDto[]; +} + +export class TvSeriesVideosDto { + @ApiPropertyOptional({ example: 1399, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [VideoDto], + description: 'Array of videos', + }) + results?: VideoDto[]; +} + +// ===================================================================== +// IMAGES TYPES +// ===================================================================== + +export class ImageDto { + @ApiPropertyOptional({ example: 1.778, description: 'Image aspect ratio' }) + aspect_ratio?: number; + + @ApiPropertyOptional({ example: 1080, description: 'Height in pixels' }) + height?: number; + + @ApiPropertyOptional({ + example: 'en', + description: 'Language code (ISO 639-1), can be null', + }) + iso_639_1?: any; + + @ApiPropertyOptional({ + example: '/fCayJrkfRaCRCTh8GqN30f8oyQF.jpg', + description: 'Image file path', + }) + file_path?: string; + + @ApiPropertyOptional({ example: 5.384, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 4, description: 'Vote count' }) + vote_count?: number; + + @ApiPropertyOptional({ example: 1920, description: 'Width in pixels' }) + width?: number; +} + +export class MovieImagesDto { + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Backdrop images', + }) + backdrops?: ImageDto[]; + + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Logo images', + }) + logos?: ImageDto[]; + + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Poster images', + }) + posters?: ImageDto[]; +} + +export class TvSeriesImagesDto { + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Backdrop images', + }) + backdrops?: ImageDto[]; + + @ApiPropertyOptional({ example: 1399, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Logo images', + }) + logos?: ImageDto[]; + + @ApiPropertyOptional({ + type: [ImageDto], + description: 'Poster images', + }) + posters?: ImageDto[]; +} + +// ===================================================================== +// EXTERNAL IDS TYPES +// ===================================================================== + +export class MovieExternalIdsDto { + @ApiPropertyOptional({ example: 550, description: 'Movie ID' }) + id?: number; + + @ApiPropertyOptional({ example: 'tt0137523', description: 'IMDb ID' }) + imdb_id?: string; + + @ApiPropertyOptional({ description: 'Wikidata ID' }) + wikidata_id?: any; + + @ApiPropertyOptional({ + example: 'FightClub', + description: 'Facebook page name', + }) + facebook_id?: string; + + @ApiPropertyOptional({ description: 'Instagram handle' }) + instagram_id?: any; + + @ApiPropertyOptional({ description: 'Twitter handle' }) + twitter_id?: any; +} + +export class TvSeriesExternalIdsDto { + @ApiPropertyOptional({ example: 1399, description: 'Series ID' }) + id?: number; + + @ApiPropertyOptional({ example: 'tt0944947', description: 'IMDb ID' }) + imdb_id?: string; + + @ApiPropertyOptional({ + example: '/m/0524b41', + description: 'Freebase MID', + }) + freebase_mid?: string; + + @ApiPropertyOptional({ + example: '/en/game_of_thrones', + description: 'Freebase ID', + }) + freebase_id?: string; + + @ApiPropertyOptional({ example: 121361, description: 'TVDB ID' }) + tvdb_id?: number; + + @ApiPropertyOptional({ example: 24493, description: 'TVRage ID' }) + tvrage_id?: number; + + @ApiPropertyOptional({ example: 'Q23572', description: 'Wikidata ID' }) + wikidata_id?: string; + + @ApiPropertyOptional({ + example: 'GameOfThrones', + description: 'Facebook page name', + }) + facebook_id?: string; + + @ApiPropertyOptional({ + example: 'gameofthrones', + description: 'Instagram handle', + }) + instagram_id?: string; + + @ApiPropertyOptional({ + example: 'GameOfThrones', + description: 'Twitter handle', + }) + twitter_id?: string; +} + +// ===================================================================== +// CONFIGURATION TYPES +// ===================================================================== + +export class ConfigurationImagesDto { + @ApiPropertyOptional({ + example: 'http://image.tmdb.org/t/p/', + description: 'Base URL for images', + }) + base_url?: string; + + @ApiPropertyOptional({ + example: 'https://image.tmdb.org/t/p/', + description: 'HTTPS base URL for images', + }) + secure_base_url?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['w300', 'w780', 'w1280', 'original'], + description: 'Available backdrop sizes', + }) + backdrop_sizes?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['w45', 'w92', 'w154', 'w185', 'w300', 'w500', 'original'], + description: 'Available logo sizes', + }) + logo_sizes?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['w92', 'w154', 'w185', 'w342', 'w500', 'w780', 'original'], + description: 'Available poster sizes', + }) + poster_sizes?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['w45', 'w185', 'h632', 'original'], + description: 'Available profile image sizes', + }) + profile_sizes?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['w92', 'w185', 'w300', 'original'], + description: 'Available still image sizes', + }) + still_sizes?: string[]; +} + +export class ConfigurationDetailsDto { + @ApiPropertyOptional({ + type: ConfigurationImagesDto, + description: 'Image configuration', + }) + images?: ConfigurationImagesDto; + + @ApiPropertyOptional({ + type: [String], + example: [ + 'adult', + 'air_date', + 'also_known_as', + 'alternative_titles', + 'biography', + 'birthday', + ], + description: 'Available change keys', + }) + change_keys?: string[]; +} + +// ===================================================================== +// MULTI SEARCH TYPES +// ===================================================================== + +export class MultiSearchResultItemDto { + @ApiPropertyOptional({ example: false, description: 'Adult content' }) + adult?: boolean; + + @ApiPropertyOptional({ + example: '/aDYSnJAK0BTVeE8osOy22Kz3SXY.jpg', + description: 'Backdrop path', + }) + backdrop_path?: string; + + @ApiPropertyOptional({ example: 11, description: 'Item ID' }) + id?: number; + + @ApiPropertyOptional({ + example: 'Star Wars', + description: 'Title (for movies)', + }) + title?: string; + + @ApiPropertyOptional({ + example: 'en', + description: 'Original language', + }) + original_language?: string; + + @ApiPropertyOptional({ + example: 'Star Wars', + description: 'Original title (for movies)', + }) + original_title?: string; + + @ApiPropertyOptional({ + example: 'Princess Leia is captured and held hostage...', + description: 'Overview', + }) + overview?: string; + + @ApiPropertyOptional({ + example: '/6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg', + description: 'Poster path', + }) + poster_path?: string; + + @ApiPropertyOptional({ + example: 'movie', + description: 'Media type', + enum: ['movie', 'tv', 'person'], + }) + media_type?: string; + + @ApiPropertyOptional({ + type: [Number], + example: [12, 28, 878], + description: 'Genre IDs', + }) + genre_ids?: number[]; + + @ApiPropertyOptional({ example: 78.047, description: 'Popularity' }) + popularity?: number; + + @ApiPropertyOptional({ + example: '1977-05-25', + description: 'Release date (for movies)', + }) + release_date?: string; + + @ApiPropertyOptional({ example: false, description: 'Has video' }) + video?: boolean; + + @ApiPropertyOptional({ example: 8.208, description: 'Vote average' }) + vote_average?: number; + + @ApiPropertyOptional({ example: 18528, description: 'Vote count' }) + vote_count?: number; + + // TV-specific fields + @ApiPropertyOptional({ + example: 'Breaking Bad', + description: 'Name (for TV)', + }) + name?: string; + + @ApiPropertyOptional({ + example: 'Breaking Bad', + description: 'Original name (for TV)', + }) + original_name?: string; + + @ApiPropertyOptional({ + example: '2008-01-20', + description: 'First air date (for TV)', + }) + first_air_date?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['US'], + description: 'Origin countries (for TV)', + }) + origin_country?: string[]; + + // Person-specific fields + @ApiPropertyOptional({ + example: 'Acting', + description: 'Known for department (for persons)', + }) + known_for_department?: string; + + @ApiPropertyOptional({ + example: '/xndWFsBlClOJFRdhSt4NBwiPq2o.jpg', + description: 'Profile path (for persons)', + }) + profile_path?: string; + + @ApiPropertyOptional({ example: 2, description: 'Gender (for persons)' }) + gender?: number; +} + +export class MultiSearchResponseDto { + @ApiPropertyOptional({ example: 1, description: 'Current page' }) + page?: number; + + @ApiPropertyOptional({ + type: [MultiSearchResultItemDto], + description: 'Search results', + }) + results?: MultiSearchResultItemDto[]; + + @ApiPropertyOptional({ example: 11, description: 'Total pages' }) + total_pages?: number; + + @ApiPropertyOptional({ example: 201, description: 'Total results' }) + total_results?: number; +} diff --git a/frontend/src/lib/apis/reiverr/reiverr.openapi.ts b/frontend/src/lib/apis/reiverr/reiverr.openapi.ts index 04df3a9..172afc2 100644 --- a/frontend/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/frontend/src/lib/apis/reiverr/reiverr.openapi.ts @@ -68,20 +68,1144 @@ export interface PlayState { lastPlayedAt: string; } +export interface GenreDto { + /** + * Genre ID + * @example 18 + */ + id?: number; + /** + * Genre name + * @example "Drama" + */ + name?: string; +} + +export interface ProductionCompanyDto { + /** + * Company ID + * @example 508 + */ + id?: number; + /** + * Company logo path + * @example "/7cxRWzi4LsVm4Utfpr1hfARNurT.png" + */ + logo_path?: string; + /** + * Company name + * @example "Regency Enterprises" + */ + name?: string; + /** + * Origin country code + * @example "US" + */ + origin_country?: string; +} + +export interface ProductionCountryDto { + /** + * Country code (ISO 3166-1) + * @example "US" + */ + iso_3166_1?: string; + /** + * Country name + * @example "United States of America" + */ + name?: string; +} + +export interface SpokenLanguageDto { + /** + * Language name in English + * @example "English" + */ + english_name?: string; + /** + * Language code (ISO 639-1) + * @example "en" + */ + iso_639_1?: string; + /** + * Language native name + * @example "English" + */ + name?: string; +} + +export interface VideoDto { + /** + * Language code (ISO 639-1) + * @example "en" + */ + iso_639_1?: string; + /** + * Country code (ISO 3166-1) + * @example "US" + */ + iso_3166_1?: string; + /** + * Video title + * @example "Official Trailer" + */ + name?: string; + /** + * Video key/ID (e.g., YouTube video ID) + * @example "SUXWAEX2jlg" + */ + key?: string; + /** + * Video platform + * @example "YouTube" + */ + site?: string; + /** + * Video resolution (e.g., 720, 1080) + * @example 1080 + */ + size?: number; + /** + * Video type + * @example "Trailer" + */ + type?: + | "Trailer" + | "Teaser" + | "Clip" + | "Featurette" + | "Behind the Scenes" + | "Bloopers"; + /** + * Whether it is official content + * @example true + */ + official?: boolean; + /** + * Publication date (ISO format) + * @example "2019-04-08T13:00:00.000Z" + */ + published_at?: string; + /** + * Video ID + * @example "533ec654c3a36854480003eb" + */ + id?: string; +} + +export interface MovieVideosDto { + /** + * Movie ID + * @example 550 + */ + id?: number; + /** Array of videos */ + results?: VideoDto[]; +} + +export interface CastMemberDto { + /** + * Adult content + * @example false + */ + adult?: boolean; + /** + * Gender + * @example 2 + */ + gender?: number; + /** + * Actor ID + * @example 819 + */ + id?: number; + /** + * Known for department + * @example "Acting" + */ + known_for_department?: string; + /** + * Actor name + * @example "Edward Norton" + */ + name?: string; + /** + * Original name + * @example "Edward Norton" + */ + original_name?: string; + /** + * Popularity + * @example 26.99 + */ + popularity?: number; + /** + * Profile path + * @example "/5XBzD5WuTyVQZeS4VI25z2moMeY.jpg" + */ + profile_path?: string; + /** + * Cast ID + * @example 4 + */ + cast_id?: number; + /** + * Character name + * @example "The Narrator" + */ + character?: string; + /** + * Credit ID + * @example "52fe4250c3a36847f800068f" + */ + credit_id?: string; + /** + * Billing order + * @example 0 + */ + order?: number; +} + +export interface CrewMemberDto { + /** + * Department + * @example "Directing" + */ + department?: string; + /** + * Job title + * @example "Director" + */ + job?: string; + /** + * Credit ID + * @example "5256c8a219c2956ff6046e77" + */ + credit_id?: string; + /** + * Adult content + * @example false + */ + adult?: boolean; + /** + * Gender + * @example 2 + */ + gender?: number; + /** + * Person ID + * @example 44797 + */ + id?: number; + /** + * Known for department + * @example "Directing" + */ + known_for_department?: string; + /** + * Name + * @example "Timothy Van Patten" + */ + name?: string; + /** + * Original name + * @example "Timothy Van Patten" + */ + original_name?: string; + /** + * Popularity + * @example 7.775 + */ + popularity?: number; + /** + * Profile path + * @example "/MzSOFrd99HRdr6pkSRSctk3kBR.jpg" + */ + profile_path?: string; +} + +export interface MovieCreditsDto { + /** + * Movie ID + * @example 550 + */ + id?: number; + /** Cast members */ + cast?: CastMemberDto[]; + /** Crew members */ + crew?: CrewMemberDto[]; +} + +export interface MovieExternalIdsDto { + /** + * Movie ID + * @example 550 + */ + id?: number; + /** + * IMDb ID + * @example "tt0137523" + */ + imdb_id?: string; + /** Wikidata ID */ + wikidata_id?: object; + /** + * Facebook page name + * @example "FightClub" + */ + facebook_id?: string; + /** Instagram handle */ + instagram_id?: object; + /** Twitter handle */ + twitter_id?: object; +} + +export interface ImageDto { + /** + * Image aspect ratio + * @example 1.778 + */ + aspect_ratio?: number; + /** + * Height in pixels + * @example 1080 + */ + height?: number; + /** + * Language code (ISO 639-1), can be null + * @example "en" + */ + iso_639_1?: object; + /** + * Image file path + * @example "/fCayJrkfRaCRCTh8GqN30f8oyQF.jpg" + */ + file_path?: string; + /** + * Vote average + * @example 5.384 + */ + vote_average?: number; + /** + * Vote count + * @example 4 + */ + vote_count?: number; + /** + * Width in pixels + * @example 1920 + */ + width?: number; +} + +export interface MovieImagesDto { + /** Backdrop images */ + backdrops?: ImageDto[]; + /** + * Movie ID + * @example 550 + */ + id?: number; + /** Logo images */ + logos?: ImageDto[]; + /** Poster images */ + posters?: ImageDto[]; +} + +export interface TmdbMovieFull { + /** + * Whether the movie is adult content + * @example false + */ + adult?: boolean; + /** + * Path to backdrop image + * @example "/hZkgoQYus5vegHoetLkCJzb17zJ.jpg" + */ + backdrop_path?: string; + /** Collection information if part of a collection */ + belongs_to_collection?: object; + /** + * Movie production budget + * @example 63000000 + */ + budget?: number; + /** Array of genres */ + genres?: GenreDto[]; + /** + * Official movie website + * @example "http://www.foxmovies.com/movies/fight-club" + */ + homepage?: string; + /** + * Movie ID + * @example 550 + */ + id?: number; + /** + * IMDb identifier + * @example "tt0137523" + */ + imdb_id?: string; + /** + * Original language code (ISO 639-1) + * @example "en" + */ + original_language?: string; + /** + * Original movie title + * @example "Fight Club" + */ + original_title?: string; + /** + * Movie plot summary + * @example "A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy." + */ + overview?: string; + /** + * Popularity score + * @example 61.416 + */ + popularity?: number; + /** + * Path to poster image + * @example "/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg" + */ + poster_path?: string; + /** Array of production companies */ + production_companies?: ProductionCompanyDto[]; + /** Array of production countries */ + production_countries?: ProductionCountryDto[]; + /** + * Release date (YYYY-MM-DD) + * @example "1999-10-15" + */ + release_date?: string; + /** + * Box office revenue + * @example 100853753 + */ + revenue?: number; + /** + * Duration in minutes + * @example 139 + */ + runtime?: number; + /** Array of spoken languages */ + spoken_languages?: SpokenLanguageDto[]; + /** + * Release status + * @example "Released" + */ + status?: string; + /** + * Movie tagline + * @example "Mischief. Mayhem. Soap." + */ + tagline?: string; + /** + * Movie title + * @example "Fight Club" + */ + title?: string; + /** + * Whether video is available + * @example false + */ + video?: boolean; + /** + * Average rating + * @example 8.433 + */ + vote_average?: number; + /** + * Number of votes + * @example 26280 + */ + vote_count?: number; + videos: MovieVideosDto; + credits: MovieCreditsDto; + external_ids: MovieExternalIdsDto; + images: MovieImagesDto; +} + export interface MovieMetadata { id?: string; tmdbId: string; - tmdbMovie?: object; + tmdbMovie?: TmdbMovieFull; name?: string; releaseDate?: string; libraryItems?: any[][]; updatedAt: string; } +export interface CreatorDto { + /** + * Creator ID + * @example 9813 + */ + id?: number; + /** + * Credit ID + * @example "5256c8c219c2956ff604858a" + */ + credit_id?: string; + /** + * Creator name + * @example "David Benioff" + */ + name?: string; + /** + * Gender (0=not specified, 1=female, 2=male) + * @example 2 + */ + gender?: number; + /** + * Profile image path + * @example "/xvNN5huL0X8yJ7h3IZfGG4O2zBD.jpg" + */ + profile_path?: string; +} + +export interface EpisodeToAirDto { + /** + * Episode ID + * @example 1551830 + */ + id?: number; + /** + * Episode name + * @example "The Iron Throne" + */ + name?: string; + /** + * Episode overview + * @example "In the aftermath of the devastating attack..." + */ + overview?: string; + /** + * Vote average + * @example 4.809 + */ + vote_average?: number; + /** + * Vote count + * @example 241 + */ + vote_count?: number; + /** + * Air date (YYYY-MM-DD) + * @example "2019-05-19" + */ + air_date?: string; + /** + * Episode number + * @example 6 + */ + episode_number?: number; + /** + * Production code + * @example "806" + */ + production_code?: string; + /** + * Runtime in minutes + * @example 80 + */ + runtime?: number; + /** + * Season number + * @example 8 + */ + season_number?: number; + /** + * Show ID + * @example 1399 + */ + show_id?: number; + /** + * Still image path + * @example "/zBi2O5EJfgTS6Ae0HdAYLm9o2nf.jpg" + */ + still_path?: string; +} + +export interface NextEpisodeToAir { + air_date?: string; +} + +export interface NetworkDto { + /** + * Network ID + * @example 49 + */ + id?: number; + /** + * Network logo path + * @example "/tuomPhY2UtuPTqqFnKMVHvSb724.png" + */ + logo_path?: string; + /** + * Network name + * @example "HBO" + */ + name?: string; + /** + * Origin country code + * @example "US" + */ + origin_country?: string; +} + +export interface SeasonDto { + /** + * Season air date + * @example "2010-12-05" + */ + air_date?: string; + /** + * Episode count + * @example 272 + */ + episode_count?: number; + /** + * Season ID + * @example 3627 + */ + id?: number; + /** + * Season name + * @example "Specials" + */ + name?: string; + /** + * Season overview + * @example "" + */ + overview?: string; + /** + * Poster path + * @example "/kMTcwNRfFKCZ0O2OaBZS0nZ2AIe.jpg" + */ + poster_path?: string; + /** + * Season number + * @example 0 + */ + season_number?: number; + /** + * Vote average + * @example 0 + */ + vote_average?: number; +} + +export interface TvSeriesDetailsDto { + /** + * Whether adult content + * @example false + */ + adult?: boolean; + /** + * Backdrop path + * @example "/6LWy0jvMpmjoS9fojNgHIKoWL05.jpg" + */ + backdrop_path?: string; + /** Array of series creators */ + created_by?: CreatorDto[]; + /** + * Array of typical episode durations + * @example [60,45] + */ + episode_run_time?: number[]; + /** + * First air date + * @example "2011-04-17" + */ + first_air_date?: string; + /** Array of genres */ + genres?: GenreDto[]; + /** + * Official homepage + * @example "http://www.hbo.com/game-of-thrones" + */ + homepage?: string; + /** + * Series ID + * @example 1399 + */ + id?: number; + /** + * Whether still in production + * @example false + */ + in_production?: boolean; + /** + * Array of language codes + * @example ["en","es"] + */ + languages?: string[]; + /** + * Most recent air date + * @example "2019-05-19" + */ + last_air_date?: string; + /** Last episode to air */ + last_episode_to_air?: EpisodeToAirDto; + /** + * Series name + * @example "Game of Thrones" + */ + name?: string; + /** Next episode to air (if any) */ + next_episode_to_air?: object; + /** Array of networks */ + networks?: NetworkDto[]; + /** + * Total number of episodes + * @example 73 + */ + number_of_episodes?: number; + /** + * Total number of seasons + * @example 8 + */ + number_of_seasons?: number; + /** + * Array of origin countries + * @example ["US","GB"] + */ + origin_country?: string[]; + /** + * Original language code + * @example "en" + */ + original_language?: string; + /** + * Original series name + * @example "Game of Thrones" + */ + original_name?: string; + /** + * Series description + * @example "Seven noble families fight for control of the mythical land of Westeros..." + */ + overview?: string; + /** + * Popularity score + * @example 346.098 + */ + popularity?: number; + /** + * Poster path + * @example "/1XS1oqL89opfnbLl8WnZY1O1uJx.jpg" + */ + poster_path?: string; + /** Array of production companies */ + production_companies?: ProductionCompanyDto[]; + /** Array of production countries */ + production_countries?: ProductionCountryDto[]; + /** Array of seasons */ + seasons?: SeasonDto[]; + /** Array of spoken languages */ + spoken_languages?: SpokenLanguageDto[]; + /** + * Series status + * @example "Ended" + */ + status?: string; + /** + * Series tagline + * @example "Winter Is Coming" + */ + tagline?: string; + /** + * Series type + * @example "Scripted" + */ + type?: string; + /** + * Average rating + * @example 8.438 + */ + vote_average?: number; + /** + * Vote count + * @example 21390 + */ + vote_count?: number; +} + +export interface AggregateRoleDto { + /** + * Credit ID + * @example "52542282760ee313280017f9" + */ + credit_id?: string; + /** + * Character name + * @example "Eddard Stark" + */ + character?: string; + /** + * Episode count + * @example 10 + */ + episode_count?: number; +} + +export interface AggregateCastMemberDto { + /** + * Adult content + * @example false + */ + adult?: boolean; + /** + * Gender + * @example 2 + */ + gender?: number; + /** + * Actor ID + * @example 239019 + */ + id?: number; + /** + * Known for department + * @example "Acting" + */ + known_for_department?: string; + /** + * Actor name + * @example "Sean Bean" + */ + name?: string; + /** + * Original name + * @example "Sean Bean" + */ + original_name?: string; + /** + * Popularity + * @example 20.991 + */ + popularity?: number; + /** + * Profile path + * @example "/kTjiABk3TJ3yI0Cto5RsvyT6V3o.jpg" + */ + profile_path?: string; + /** Roles played */ + roles?: AggregateRoleDto[]; + /** + * Total episode count + * @example 9 + */ + total_episode_count?: number; + /** + * Billing order + * @example 0 + */ + order?: number; +} + +export interface AggregateJobDto { + /** + * Credit ID + * @example "5256c8a019c2956ff6046e1b" + */ + credit_id?: string; + /** + * Job title + * @example "Director" + */ + job?: string; + /** + * Episode count + * @example 22 + */ + episode_count?: number; +} + +export interface AggregateCrewMemberDto { + /** + * Adult content + * @example false + */ + adult?: boolean; + /** + * Gender + * @example 2 + */ + gender?: number; + /** + * Person ID + * @example 44797 + */ + id?: number; + /** + * Known for department + * @example "Directing" + */ + known_for_department?: string; + /** + * Name + * @example "Timothy Van Patten" + */ + name?: string; + /** + * Original name + * @example "Timothy Van Patten" + */ + original_name?: string; + /** + * Popularity + * @example 7.775 + */ + popularity?: number; + /** + * Profile path + * @example "/MzSOFrd99HRdr6pkSRSctk3kBR.jpg" + */ + profile_path?: string; + /** Jobs performed */ + jobs?: AggregateJobDto[]; + /** + * Department + * @example "Directing" + */ + department?: string; + /** + * Total episode count + * @example 22 + */ + total_episode_count?: number; +} + +export interface TvSeriesAggregateCreditsDto { + /** + * Series ID + * @example 1399 + */ + id?: number; + /** Cast members */ + cast?: AggregateCastMemberDto[]; + /** Crew members */ + crew?: AggregateCrewMemberDto[]; +} + +export interface TvSeriesExternalIdsDto { + /** + * Series ID + * @example 1399 + */ + id?: number; + /** + * IMDb ID + * @example "tt0944947" + */ + imdb_id?: string; + /** + * Freebase MID + * @example "/m/0524b41" + */ + freebase_mid?: string; + /** + * Freebase ID + * @example "/en/game_of_thrones" + */ + freebase_id?: string; + /** + * TVDB ID + * @example 121361 + */ + tvdb_id?: number; + /** + * TVRage ID + * @example 24493 + */ + tvrage_id?: number; + /** + * Wikidata ID + * @example "Q23572" + */ + wikidata_id?: string; + /** + * Facebook page name + * @example "GameOfThrones" + */ + facebook_id?: string; + /** + * Instagram handle + * @example "gameofthrones" + */ + instagram_id?: string; + /** + * Twitter handle + * @example "GameOfThrones" + */ + twitter_id?: string; +} + +export interface TvSeriesImagesDto { + /** Backdrop images */ + backdrops?: ImageDto[]; + /** + * Series ID + * @example 1399 + */ + id?: number; + /** Logo images */ + logos?: ImageDto[]; + /** Poster images */ + posters?: ImageDto[]; +} + +export interface TmdbSeriesFull { + /** + * Whether adult content + * @example false + */ + adult?: boolean; + /** + * Backdrop path + * @example "/6LWy0jvMpmjoS9fojNgHIKoWL05.jpg" + */ + backdrop_path?: string; + /** Array of series creators */ + created_by?: CreatorDto[]; + /** + * Array of typical episode durations + * @example [60,45] + */ + episode_run_time?: number[]; + /** + * First air date + * @example "2011-04-17" + */ + first_air_date?: string; + /** Array of genres */ + genres?: GenreDto[]; + /** + * Official homepage + * @example "http://www.hbo.com/game-of-thrones" + */ + homepage?: string; + /** + * Series ID + * @example 1399 + */ + id?: number; + /** + * Whether still in production + * @example false + */ + in_production?: boolean; + /** + * Array of language codes + * @example ["en","es"] + */ + languages?: string[]; + /** + * Most recent air date + * @example "2019-05-19" + */ + last_air_date?: string; + /** Last episode to air */ + last_episode_to_air?: EpisodeToAirDto; + /** + * Series name + * @example "Game of Thrones" + */ + name?: string; + /** Next episode to air (if any) */ + next_episode_to_air?: NextEpisodeToAir; + /** Array of networks */ + networks?: NetworkDto[]; + /** + * Total number of episodes + * @example 73 + */ + number_of_episodes?: number; + /** + * Total number of seasons + * @example 8 + */ + number_of_seasons?: number; + /** + * Array of origin countries + * @example ["US","GB"] + */ + origin_country?: string[]; + /** + * Original language code + * @example "en" + */ + original_language?: string; + /** + * Original series name + * @example "Game of Thrones" + */ + original_name?: string; + /** + * Series description + * @example "Seven noble families fight for control of the mythical land of Westeros..." + */ + overview?: string; + /** + * Popularity score + * @example 346.098 + */ + popularity?: number; + /** + * Poster path + * @example "/1XS1oqL89opfnbLl8WnZY1O1uJx.jpg" + */ + poster_path?: string; + /** Array of production companies */ + production_companies?: ProductionCompanyDto[]; + /** Array of production countries */ + production_countries?: ProductionCountryDto[]; + /** Array of seasons */ + seasons?: any[][]; + /** Array of spoken languages */ + spoken_languages?: SpokenLanguageDto[]; + /** + * Series status + * @example "Ended" + */ + status?: string; + /** + * Series tagline + * @example "Winter Is Coming" + */ + tagline?: string; + /** + * Series type + * @example "Scripted" + */ + type?: string; + /** + * Average rating + * @example 8.438 + */ + vote_average?: number; + /** + * Vote count + * @example 21390 + */ + vote_count?: number; + videos: TvSeriesDetailsDto; + aggregate_credits: TvSeriesAggregateCreditsDto; + external_ids: TvSeriesExternalIdsDto; + images: TvSeriesImagesDto; +} + export interface SeriesMetadata { id?: string; tmdbId: string; - tmdbSeries?: object; + tmdbSeries?: TmdbSeriesFull; name?: string; firstReleaseDate?: string; lastReleaseDate?: string; @@ -743,21 +1867,6 @@ export interface BulkUpdatePlayStateDto { playStates: UpdatePlayStateDto[]; } -export interface NextEpisodeToAir { - air_date?: string; -} - -export interface Season { - air_date?: string; - episode_count?: number; - id?: number; - name?: string; - overview?: string; - poster_path?: string; - season_number?: number; - vote_average?: number; -} - export interface TmdbItemDto { id?: number; poster_path?: string; @@ -769,7 +1878,7 @@ export interface TmdbItemDto { first_air_date?: string; last_air_date?: string; next_episode_to_air?: NextEpisodeToAir; - seasons?: Season[]; + seasons?: SeasonDto[]; } export interface LibraryItemDto { @@ -1891,6 +3000,36 @@ export class Api< }), }; metadata = { + /** + * No description + * + * @tags metadata + * @name GetMovie + * @request GET:/api/metadata/movie/{tmdbId} + */ + getMovie: (tmdbId: string, params: RequestParams = {}) => + this.request({ + path: `/api/metadata/movie/${tmdbId}`, + method: "GET", + format: "json", + ...params, + }), + + /** + * No description + * + * @tags metadata + * @name GetSeries + * @request GET:/api/metadata/series/{tmdbId} + */ + getSeries: (tmdbId: string, params: RequestParams = {}) => + this.request({ + path: `/api/metadata/series/${tmdbId}`, + method: "GET", + format: "json", + ...params, + }), + /** * No description * @@ -2090,7 +3229,7 @@ export class Api< * No description * * @tags library - * @name updateLibraryItem + * @name UpdateLibraryItem * @request PUT:/api/users/{userId}/library/tmdb/{tmdbId} */ updateLibraryItem: (