Merge branch 'develop' of https://github.com/sct/overseerr into jellyfin-support
This commit is contained in:
133
server/api/github.ts
Normal file
133
server/api/github.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import cacheManager from '../lib/cache';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface GitHubRelease {
|
||||
url: string;
|
||||
assets_url: string;
|
||||
upload_url: string;
|
||||
html_url: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
name: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
created_at: string;
|
||||
published_at: string;
|
||||
tarball_url: string;
|
||||
zipball_url: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface GithubCommit {
|
||||
sha: string;
|
||||
node_id: string;
|
||||
commit: {
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
committer: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
message: string;
|
||||
tree: {
|
||||
sha: string;
|
||||
url: string;
|
||||
};
|
||||
url: string;
|
||||
comment_count: number;
|
||||
verification: {
|
||||
verified: boolean;
|
||||
reason: string;
|
||||
signature: string;
|
||||
payload: string;
|
||||
};
|
||||
};
|
||||
url: string;
|
||||
html_url: string;
|
||||
comments_url: string;
|
||||
parents: [
|
||||
{
|
||||
sha: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class GithubAPI extends ExternalAPI {
|
||||
constructor() {
|
||||
super(
|
||||
'https://api.github.com',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('github').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getOverseerrReleases({
|
||||
take = 20,
|
||||
}: {
|
||||
take?: number;
|
||||
} = {}): Promise<GitHubRelease[]> {
|
||||
try {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/sct/overseerr/releases',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
||||
{ label: 'GitHub API', errorMessage: e.message }
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async getOverseerrCommits({
|
||||
take = 20,
|
||||
branch = 'develop',
|
||||
}: {
|
||||
take?: number;
|
||||
branch?: string;
|
||||
} = {}): Promise<GithubCommit[]> {
|
||||
try {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/sct/overseerr/commits',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
branch,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
||||
{ label: 'GitHub API', errorMessage: e.message }
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GithubAPI;
|
||||
@@ -1,5 +1,5 @@
|
||||
import NodePlexAPI from 'plex-api';
|
||||
import { getSettings, PlexSettings } from '../lib/settings';
|
||||
import { getSettings, Library, PlexSettings } from '../lib/settings';
|
||||
|
||||
export interface PlexLibraryItem {
|
||||
ratingKey: string;
|
||||
@@ -11,11 +11,16 @@ export interface PlexLibraryItem {
|
||||
grandparentGuid?: string;
|
||||
addedAt: number;
|
||||
updatedAt: number;
|
||||
Guid?: {
|
||||
id: string;
|
||||
}[];
|
||||
type: 'movie' | 'show' | 'season' | 'episode';
|
||||
Media: Media[];
|
||||
}
|
||||
|
||||
interface PlexLibraryResponse {
|
||||
MediaContainer: {
|
||||
totalSize: number;
|
||||
Metadata: PlexLibraryItem[];
|
||||
};
|
||||
}
|
||||
@@ -118,7 +123,7 @@ class PlexAPI {
|
||||
options: {
|
||||
identifier: settings.clientId,
|
||||
product: 'Overseerr',
|
||||
deviceName: settings.main.applicationTitle,
|
||||
deviceName: 'Overseerr',
|
||||
platform: 'Overseerr',
|
||||
},
|
||||
});
|
||||
@@ -137,12 +142,50 @@ class PlexAPI {
|
||||
return response.MediaContainer.Directory;
|
||||
}
|
||||
|
||||
public async getLibraryContents(id: string): Promise<PlexLibraryItem[]> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>(
|
||||
`/library/sections/${id}/all`
|
||||
);
|
||||
public async syncLibraries(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
|
||||
return response.MediaContainer.Metadata;
|
||||
const libraries = await this.getLibraries();
|
||||
|
||||
const newLibraries: Library[] = libraries
|
||||
// Remove libraries that are not movie or show
|
||||
.filter((library) => library.type === 'movie' || library.type === 'show')
|
||||
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
|
||||
.filter((library) => library.agent !== 'com.plexapp.agents.none')
|
||||
.map((library) => {
|
||||
const existing = settings.plex.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
);
|
||||
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
type: library.type,
|
||||
lastScan: existing?.lastScan,
|
||||
};
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
settings.save();
|
||||
}
|
||||
|
||||
public async getLibraryContents(
|
||||
id: string,
|
||||
{ offset = 0, size = 50 }: { offset?: number; size?: number } = {}
|
||||
): Promise<{ totalSize: number; items: PlexLibraryItem[] }> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>({
|
||||
uri: `/library/sections/${id}/all?includeGuids=1`,
|
||||
extraHeaders: {
|
||||
'X-Plex-Container-Start': `${offset}`,
|
||||
'X-Plex-Container-Size': `${size}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalSize: response.MediaContainer.totalSize,
|
||||
items: response.MediaContainer.Metadata ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
public async getMetadata(
|
||||
@@ -166,10 +209,17 @@ class PlexAPI {
|
||||
return response.MediaContainer.Metadata;
|
||||
}
|
||||
|
||||
public async getRecentlyAdded(id: string): Promise<PlexLibraryItem[]> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>(
|
||||
`/library/sections/${id}/recentlyAdded`
|
||||
);
|
||||
public async getRecentlyAdded(
|
||||
id: string,
|
||||
options: { addedAt: number } = {
|
||||
addedAt: Date.now() - 1000 * 60 * 60,
|
||||
}
|
||||
): Promise<PlexLibraryItem[]> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>({
|
||||
uri: `/library/sections/${id}/all?sort=addedAt%3Adesc&addedAt>>=${Math.floor(
|
||||
options.addedAt / 1000
|
||||
)}`,
|
||||
});
|
||||
|
||||
return response.MediaContainer.Metadata;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ interface FriendResponse {
|
||||
email: string;
|
||||
thumb: string;
|
||||
};
|
||||
Server: ServerResponse[];
|
||||
Server?: ServerResponse[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
@@ -232,7 +232,7 @@ class PlexTvAPI {
|
||||
);
|
||||
}
|
||||
|
||||
return !!user.Server.find(
|
||||
return !!user.Server?.find(
|
||||
(server) => server.$.machineIdentifier === settings.plex.machineId
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,39 +1,28 @@
|
||||
import cacheManager from '../lib/cache';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RTMovieOldSearchResult {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number;
|
||||
ratings: {
|
||||
critics_rating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
||||
critics_score: number;
|
||||
audience_rating: 'Upright' | 'Spilled';
|
||||
audience_score: number;
|
||||
};
|
||||
links: {
|
||||
self: string;
|
||||
alternate: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RTTvSearchResult {
|
||||
title: string;
|
||||
meterClass: 'fresh' | 'rotten';
|
||||
interface RTSearchResult {
|
||||
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
|
||||
meterScore: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface RTTvSearchResult extends RTSearchResult {
|
||||
title: string;
|
||||
startYear: number;
|
||||
endYear: number;
|
||||
}
|
||||
|
||||
interface RTMovieSearchResponse {
|
||||
total: number;
|
||||
movies: RTMovieOldSearchResult[];
|
||||
interface RTMovieSearchResult extends RTSearchResult {
|
||||
name: string;
|
||||
url: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
interface RTMultiSearchResponse {
|
||||
tvCount: number;
|
||||
tvSeries: RTTvSearchResult[];
|
||||
movieCount: number;
|
||||
movies: RTMovieSearchResult[];
|
||||
}
|
||||
|
||||
export interface RTRating {
|
||||
@@ -88,19 +77,19 @@ class RottenTomatoes extends ExternalAPI {
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
||||
params: { q: name },
|
||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||
params: { q: name, limit: 10 },
|
||||
});
|
||||
|
||||
// First, attempt to match exact name and year
|
||||
let movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title === name
|
||||
(movie) => movie.year === year && movie.name === name
|
||||
);
|
||||
|
||||
// If we don't find a movie, try to match partial name and year
|
||||
if (!movie) {
|
||||
movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title.includes(name)
|
||||
(movie) => movie.year === year && movie.name.includes(name)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +100,7 @@ class RottenTomatoes extends ExternalAPI {
|
||||
|
||||
// One last try, try exact name match only
|
||||
if (!movie) {
|
||||
movie = data.movies.find((movie) => movie.title === name);
|
||||
movie = data.movies.find((movie) => movie.name === name);
|
||||
}
|
||||
|
||||
if (!movie) {
|
||||
@@ -119,12 +108,15 @@ class RottenTomatoes extends ExternalAPI {
|
||||
}
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
url: movie.links.alternate,
|
||||
criticsRating: movie.ratings.critics_rating,
|
||||
criticsScore: movie.ratings.critics_score,
|
||||
audienceRating: movie.ratings.audience_rating,
|
||||
audienceScore: movie.ratings.audience_score,
|
||||
title: movie.name,
|
||||
url: `https://www.rottentomatoes.com${movie.url}`,
|
||||
criticsRating:
|
||||
movie.meterClass === 'certified_fresh'
|
||||
? 'Certified Fresh'
|
||||
: movie.meterClass === 'fresh'
|
||||
? 'Fresh'
|
||||
: 'Rotten',
|
||||
criticsScore: movie.meterScore,
|
||||
year: movie.year,
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
169
server/api/servarr/base.ts
Normal file
169
server/api/servarr/base.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
||||
import { DVRSettings } from '../../lib/settings';
|
||||
import ExternalAPI from '../externalapi';
|
||||
|
||||
export interface RootFolder {
|
||||
id: number;
|
||||
path: string;
|
||||
freeSpace: number;
|
||||
totalSpace: number;
|
||||
unmappedFolders: {
|
||||
name: string;
|
||||
path: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface QualityProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
indexer: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface QueueResponse<QueueItemAppendT> {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
records: (QueueItem & QueueItemAppendT)[];
|
||||
}
|
||||
|
||||
class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
static buildUrl(settings: DVRSettings, path?: string): string {
|
||||
return `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
settings.port
|
||||
}${settings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
protected apiName: string;
|
||||
|
||||
constructor({
|
||||
url,
|
||||
apiKey,
|
||||
cacheName,
|
||||
apiName,
|
||||
}: {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
cacheName: AvailableCacheIds;
|
||||
apiName: string;
|
||||
}) {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache(cacheName).data,
|
||||
}
|
||||
);
|
||||
|
||||
this.apiName = apiName;
|
||||
}
|
||||
|
||||
public getProfiles = async (): Promise<QualityProfile[]> => {
|
||||
try {
|
||||
const data = await this.getRolling<QualityProfile[]>(
|
||||
`/qualityProfile`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve profiles: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getRootFolders = async (): Promise<RootFolder[]> => {
|
||||
try {
|
||||
const data = await this.getRolling<RootFolder[]>(
|
||||
`/rootfolder`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve root folders: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
|
||||
`/queue`
|
||||
);
|
||||
|
||||
return response.data.records;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getTags = async (): Promise<Tag[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<Tag[]>(`/tag`);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
||||
try {
|
||||
const response = await this.axios.post<Tag>(`/tag`, {
|
||||
label,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
protected async runCommand(
|
||||
commandName: string,
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.axios.post(`/command`, {
|
||||
name: commandName,
|
||||
...options,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ServarrBase;
|
||||
@@ -1,12 +1,11 @@
|
||||
import cacheManager from '../lib/cache';
|
||||
import { RadarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
import logger from '../../logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
interface RadarrMovieOptions {
|
||||
title: string;
|
||||
qualityProfileId: number;
|
||||
minimumAvailability: string;
|
||||
tags: number[];
|
||||
profileId: number;
|
||||
year: number;
|
||||
rootFolderPath: string;
|
||||
@@ -32,65 +31,9 @@ export interface RadarrMovie {
|
||||
hasFile: boolean;
|
||||
}
|
||||
|
||||
export interface RadarrRootFolder {
|
||||
id: number;
|
||||
path: string;
|
||||
freeSpace: number;
|
||||
totalSpace: number;
|
||||
unmappedFolders: {
|
||||
name: string;
|
||||
path: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface RadarrProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
movieId: number;
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
indexer: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface QueueResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
records: QueueItem[];
|
||||
}
|
||||
|
||||
class RadarrAPI extends ExternalAPI {
|
||||
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
|
||||
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
radarrSettings.hostname
|
||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('radarr').data,
|
||||
}
|
||||
);
|
||||
super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr' });
|
||||
}
|
||||
|
||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||
@@ -129,7 +72,8 @@ class RadarrAPI extends ExternalAPI {
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving movie by TMDb ID', {
|
||||
label: 'Radarr API',
|
||||
message: e.message,
|
||||
errorMessage: e.message,
|
||||
tmdbId: id,
|
||||
});
|
||||
throw new Error('Movie not found');
|
||||
}
|
||||
@@ -146,12 +90,13 @@ class RadarrAPI extends ExternalAPI {
|
||||
'Title already exists and is available. Skipping add and returning success',
|
||||
{
|
||||
label: 'Radarr',
|
||||
movie,
|
||||
}
|
||||
);
|
||||
return movie;
|
||||
}
|
||||
|
||||
// movie exists in radarr but is neither downloaded nor monitored
|
||||
// movie exists in Radarr but is neither downloaded nor monitored
|
||||
if (movie.id && !movie.monitored) {
|
||||
const response = await this.axios.put<RadarrMovie>(`/movie`, {
|
||||
...movie,
|
||||
@@ -162,6 +107,7 @@ class RadarrAPI extends ExternalAPI {
|
||||
minimumAvailability: options.minimumAvailability,
|
||||
tmdbId: options.tmdbId,
|
||||
year: options.year,
|
||||
tags: options.tags,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
monitored: options.monitored,
|
||||
addOptions: {
|
||||
@@ -171,16 +117,25 @@ class RadarrAPI extends ExternalAPI {
|
||||
|
||||
if (response.data.monitored) {
|
||||
logger.info(
|
||||
'Found existing title in Radarr and set it to monitored. Returning success',
|
||||
{ label: 'Radarr' }
|
||||
'Found existing title in Radarr and set it to monitored.',
|
||||
{
|
||||
label: 'Radarr',
|
||||
movieId: response.data.id,
|
||||
movieTitle: response.data.title,
|
||||
}
|
||||
);
|
||||
logger.debug('Radarr update details', {
|
||||
label: 'Radarr',
|
||||
movie: response.data,
|
||||
});
|
||||
|
||||
if (options.searchNow) {
|
||||
this.searchMovie(response.data.id);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} else {
|
||||
logger.error('Failed to update existing movie in Radarr', {
|
||||
logger.error('Failed to update existing movie in Radarr.', {
|
||||
label: 'Radarr',
|
||||
options,
|
||||
});
|
||||
@@ -206,6 +161,7 @@ class RadarrAPI extends ExternalAPI {
|
||||
year: options.year,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
monitored: options.monitored,
|
||||
tags: options.tags,
|
||||
addOptions: {
|
||||
searchForMovie: options.searchNow,
|
||||
},
|
||||
@@ -239,43 +195,25 @@ class RadarrAPI extends ExternalAPI {
|
||||
}
|
||||
};
|
||||
|
||||
public getProfiles = async (): Promise<RadarrProfile[]> => {
|
||||
public async searchMovie(movieId: number): Promise<void> {
|
||||
logger.info('Executing movie search command', {
|
||||
label: 'Radarr API',
|
||||
movieId,
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await this.getRolling<RadarrProfile[]>(
|
||||
`/qualityProfile`,
|
||||
undefined,
|
||||
3600
|
||||
await this.runCommand('MoviesSearch', { movieIds: [movieId] });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while executing Radarr movie search.',
|
||||
{
|
||||
label: 'Radarr API',
|
||||
errorMessage: e.message,
|
||||
movieId,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
|
||||
try {
|
||||
const data = await this.getRolling<RadarrRootFolder[]>(
|
||||
`/rootfolder`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getQueue = async (): Promise<QueueItem[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse>(`/queue`);
|
||||
|
||||
return response.data.records;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default RadarrAPI;
|
||||
@@ -1,7 +1,5 @@
|
||||
import cacheManager from '../lib/cache';
|
||||
import { SonarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
import logger from '../../logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
interface SonarrSeason {
|
||||
seasonNumber: number;
|
||||
@@ -49,7 +47,7 @@ export interface SonarrSeries {
|
||||
titleSlug: string;
|
||||
certification: string;
|
||||
genres: string[];
|
||||
tags: string[];
|
||||
tags: number[];
|
||||
added: string;
|
||||
ratings: {
|
||||
votes: number;
|
||||
@@ -65,49 +63,6 @@ export interface SonarrSeries {
|
||||
};
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
indexer: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface QueueResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
records: QueueItem[];
|
||||
}
|
||||
|
||||
interface SonarrProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SonarrRootFolder {
|
||||
id: number;
|
||||
path: string;
|
||||
freeSpace: number;
|
||||
totalSpace: number;
|
||||
unmappedFolders: {
|
||||
name: string;
|
||||
path: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface AddSeriesOptions {
|
||||
tvdbid: number;
|
||||
title: string;
|
||||
@@ -116,6 +71,7 @@ interface AddSeriesOptions {
|
||||
seasons: number[];
|
||||
seasonFolder: boolean;
|
||||
rootFolderPath: string;
|
||||
tags?: number[];
|
||||
seriesType: SonarrSeries['seriesType'];
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
@@ -126,23 +82,9 @@ export interface LanguageProfile {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class SonarrAPI extends ExternalAPI {
|
||||
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
|
||||
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
sonarrSettings.hostname
|
||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('sonarr').data,
|
||||
}
|
||||
);
|
||||
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' });
|
||||
}
|
||||
|
||||
public async getSeries(): Promise<SonarrSeries[]> {
|
||||
@@ -151,7 +93,7 @@ class SonarrAPI extends ExternalAPI {
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve series: ${e.message}`);
|
||||
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +113,8 @@ class SonarrAPI extends ExternalAPI {
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving series by series title', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
errorMessage: e.message,
|
||||
title,
|
||||
});
|
||||
throw new Error('No series found');
|
||||
}
|
||||
@@ -193,7 +136,8 @@ class SonarrAPI extends ExternalAPI {
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving series by tvdb ID', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
errorMessage: e.message,
|
||||
tvdbId: id,
|
||||
});
|
||||
throw new Error('Series not found');
|
||||
}
|
||||
@@ -205,26 +149,30 @@ class SonarrAPI extends ExternalAPI {
|
||||
|
||||
// If the series already exists, we will simply just update it
|
||||
if (series.id) {
|
||||
series.tags = options.tags ?? series.tags;
|
||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||
|
||||
series.addOptions = {
|
||||
ignoreEpisodesWithFiles: true,
|
||||
searchForMissingEpisodes: options.searchNow,
|
||||
};
|
||||
|
||||
const newSeriesResponse = await this.axios.put<SonarrSeries>(
|
||||
'/series',
|
||||
series
|
||||
);
|
||||
|
||||
if (newSeriesResponse.data.id) {
|
||||
logger.info('Sonarr accepted request. Updated existing series', {
|
||||
logger.info('Updated existing series in Sonarr.', {
|
||||
label: 'Sonarr',
|
||||
seriesId: newSeriesResponse.data.id,
|
||||
seriesTitle: newSeriesResponse.data.title,
|
||||
});
|
||||
logger.debug('Sonarr update details', {
|
||||
label: 'Sonarr',
|
||||
movie: newSeriesResponse.data,
|
||||
});
|
||||
|
||||
if (options.searchNow) {
|
||||
this.searchSeries(newSeriesResponse.data.id);
|
||||
}
|
||||
|
||||
return newSeriesResponse.data;
|
||||
} else {
|
||||
logger.error('Failed to update series in Sonarr', {
|
||||
label: 'Sonarr',
|
||||
@@ -232,8 +180,6 @@ class SonarrAPI extends ExternalAPI {
|
||||
});
|
||||
throw new Error('Failed to update series in Sonarr');
|
||||
}
|
||||
|
||||
return newSeriesResponse.data;
|
||||
}
|
||||
|
||||
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
||||
@@ -251,6 +197,7 @@ class SonarrAPI extends ExternalAPI {
|
||||
monitored: false,
|
||||
}))
|
||||
),
|
||||
tags: options.tags,
|
||||
seasonFolder: options.seasonFolder,
|
||||
monitored: options.monitored,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
@@ -281,53 +228,13 @@ class SonarrAPI extends ExternalAPI {
|
||||
logger.error('Something went wrong while adding a series to Sonarr.', {
|
||||
label: 'Sonarr API',
|
||||
errorMessage: e.message,
|
||||
error: e,
|
||||
options,
|
||||
response: e?.response?.data,
|
||||
});
|
||||
throw new Error('Failed to add series');
|
||||
}
|
||||
}
|
||||
|
||||
public async getProfiles(): Promise<SonarrProfile[]> {
|
||||
try {
|
||||
const data = await this.getRolling<SonarrProfile[]>(
|
||||
'/qualityProfile',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong while retrieving Sonarr profiles.', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
});
|
||||
throw new Error('Failed to get profiles');
|
||||
}
|
||||
}
|
||||
|
||||
public async getRootFolders(): Promise<SonarrRootFolder[]> {
|
||||
try {
|
||||
const data = await this.getRolling<SonarrRootFolder[]>(
|
||||
'/rootfolder',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while retrieving Sonarr root folders.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
throw new Error('Failed to get root folders');
|
||||
}
|
||||
}
|
||||
|
||||
public async getLanguageProfiles(): Promise<LanguageProfile[]> {
|
||||
try {
|
||||
const data = await this.getRolling<LanguageProfile[]>(
|
||||
@@ -342,7 +249,7 @@ class SonarrAPI extends ExternalAPI {
|
||||
'Something went wrong while retrieving Sonarr language profiles.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -350,6 +257,26 @@ class SonarrAPI extends ExternalAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public async searchSeries(seriesId: number): Promise<void> {
|
||||
logger.info('Executing series search command.', {
|
||||
label: 'Sonarr API',
|
||||
seriesId,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.runCommand('SeriesSearch', { seriesId });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while executing Sonarr series search.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
errorMessage: e.message,
|
||||
seriesId,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildSeasonList(
|
||||
seasons: number[],
|
||||
existingSeasons?: SonarrSeason[]
|
||||
@@ -374,16 +301,6 @@ class SonarrAPI extends ExternalAPI {
|
||||
|
||||
return newSeasons;
|
||||
}
|
||||
|
||||
public getQueue = async (): Promise<QueueItem[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse>(`/queue`);
|
||||
|
||||
return response.data.records;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SonarrAPI;
|
||||
@@ -4,10 +4,14 @@ import ExternalAPI from '../externalapi';
|
||||
import {
|
||||
TmdbCollection,
|
||||
TmdbExternalIdResponse,
|
||||
TmdbGenre,
|
||||
TmdbGenresResult,
|
||||
TmdbLanguage,
|
||||
TmdbMovieDetails,
|
||||
TmdbNetwork,
|
||||
TmdbPersonCombinedCredits,
|
||||
TmdbPersonDetail,
|
||||
TmdbProductionCompany,
|
||||
TmdbRegion,
|
||||
TmdbSearchMovieResponse,
|
||||
TmdbSearchMultiResponse,
|
||||
@@ -30,6 +34,9 @@ interface DiscoverMovieOptions {
|
||||
language?: string;
|
||||
primaryReleaseDateGte?: string;
|
||||
primaryReleaseDateLte?: string;
|
||||
originalLanguage?: string;
|
||||
genre?: number;
|
||||
studio?: number;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
@@ -53,6 +60,9 @@ interface DiscoverTvOptions {
|
||||
firstAirDateGte?: string;
|
||||
firstAirDateLte?: string;
|
||||
includeEmptyReleaseDate?: boolean;
|
||||
originalLanguage?: string;
|
||||
genre?: number;
|
||||
network?: number;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
@@ -120,7 +130,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,7 +152,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch person combined credits: ${e.message}`
|
||||
`[TMDb] Failed to fetch person combined credits: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -160,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos,release_dates',
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,release_dates,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
@@ -168,7 +179,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -186,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings',
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
@@ -194,7 +205,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -220,7 +231,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -246,7 +257,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +283,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +309,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +336,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch tv recommendations: ${e.message}`
|
||||
`[TMDb] Failed to fetch TV recommendations: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -349,7 +360,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv similar: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +371,9 @@ class TheMovieDb extends ExternalAPI {
|
||||
language = 'en',
|
||||
primaryReleaseDateGte,
|
||||
primaryReleaseDateLte,
|
||||
originalLanguage,
|
||||
genre,
|
||||
studio,
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
@@ -368,27 +382,31 @@ class TheMovieDb extends ExternalAPI {
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
with_release_type: '3|2',
|
||||
region: this.region,
|
||||
with_original_language: this.originalLanguage,
|
||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
||||
'primary_release_date.gte': primaryReleaseDateGte,
|
||||
'primary_release_date.lte': primaryReleaseDateLte,
|
||||
with_genres: genre,
|
||||
with_companies: studio,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
language = 'en',
|
||||
firstAirDateGte,
|
||||
firstAirDateLte,
|
||||
includeEmptyReleaseDate = false,
|
||||
originalLanguage,
|
||||
genre,
|
||||
network,
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
@@ -399,14 +417,16 @@ class TheMovieDb extends ExternalAPI {
|
||||
region: this.region,
|
||||
'first_air_date.gte': firstAirDateGte,
|
||||
'first_air_date.lte': firstAirDateLte,
|
||||
with_original_language: this.originalLanguage,
|
||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||
with_genres: genre,
|
||||
with_networks: network,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover tv: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -432,7 +452,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -459,7 +479,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -482,7 +502,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -505,7 +525,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -537,7 +557,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,11 +584,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDB] Failed to find a title with the provided IMDB id'
|
||||
'[TMDb] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get movie by external imdb ID: ${e.message}`
|
||||
`[TMDb] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -595,12 +615,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
return tvshow;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[TMDB] Failed to find a TV show with the provided TVDB ID: ${tvdbId}`
|
||||
);
|
||||
throw new Error(`No show returned from API for ID ${tvdbId}`);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||
`[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -624,7 +642,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,7 +658,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return regions;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,7 +674,131 @@ class TheMovieDb extends ExternalAPI {
|
||||
|
||||
return languages;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`);
|
||||
throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getStudio(studioId: number): Promise<TmdbProductionCompany> {
|
||||
try {
|
||||
const data = await this.get<TmdbProductionCompany>(
|
||||
`/company/${studioId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getNetwork(networkId: number): Promise<TmdbNetwork> {
|
||||
try {
|
||||
const data = await this.get<TmdbNetwork>(`/network/${networkId}`);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieGenres({
|
||||
language = 'en',
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
if (
|
||||
!language.startsWith('en') &&
|
||||
data.genres.some((genre) => !genre.name)
|
||||
) {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
data.genres
|
||||
.filter((genre) => !genre.name)
|
||||
.forEach((genre) => {
|
||||
genre.name =
|
||||
englishData.genres.find(
|
||||
(englishGenre) => englishGenre.id === genre.id
|
||||
)?.name ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
const movieGenres = sortBy(
|
||||
data.genres.filter((genre) => genre.name),
|
||||
'name'
|
||||
);
|
||||
|
||||
return movieGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvGenres({
|
||||
language = 'en',
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
if (
|
||||
!language.startsWith('en') &&
|
||||
data.genres.some((genre) => !genre.name)
|
||||
) {
|
||||
const englishData = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
params: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
data.genres
|
||||
.filter((genre) => !genre.name)
|
||||
.forEach((genre) => {
|
||||
genre.name =
|
||||
englishData.genres.find(
|
||||
(englishGenre) => englishGenre.id === genre.id
|
||||
)?.name ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
const tvGenres = sortBy(
|
||||
data.genres.filter((genre) => genre.name),
|
||||
'name'
|
||||
);
|
||||
|
||||
return tvGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,16 @@ export interface TmdbExternalIds {
|
||||
twitter_id?: string;
|
||||
}
|
||||
|
||||
export interface TmdbProductionCompany {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
homepage?: string;
|
||||
headquarters?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieDetails {
|
||||
id: number;
|
||||
imdb_id?: string;
|
||||
@@ -125,12 +135,7 @@ export interface TmdbMovieDetails {
|
||||
original_title: string;
|
||||
overview?: string;
|
||||
popularity: number;
|
||||
production_companies: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path?: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
production_companies: TmdbProductionCompany[];
|
||||
production_countries: {
|
||||
iso_3166_1: string;
|
||||
name: string;
|
||||
@@ -161,6 +166,10 @@ export interface TmdbMovieDetails {
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
'watch/providers'?: {
|
||||
id: number;
|
||||
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||
};
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
@@ -227,12 +236,7 @@ export interface TmdbTvDetails {
|
||||
last_episode_to_air?: TmdbTvEpisodeResult;
|
||||
name: string;
|
||||
next_episode_to_air?: TmdbTvEpisodeResult;
|
||||
networks: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
networks: TmdbNetwork[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
origin_country: string[];
|
||||
@@ -254,6 +258,7 @@ export interface TmdbTvDetails {
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
type: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
@@ -268,6 +273,10 @@ export interface TmdbTvDetails {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
'watch/providers'?: {
|
||||
id: number;
|
||||
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||
};
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
@@ -305,6 +314,7 @@ export interface TmdbKeyword {
|
||||
export interface TmdbPersonDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
birthday: string;
|
||||
deathday: string;
|
||||
known_for_department: string;
|
||||
also_known_as?: string[];
|
||||
@@ -381,3 +391,34 @@ export interface TmdbLanguage {
|
||||
english_name: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbGenresResult {
|
||||
genres: TmdbGenre[];
|
||||
}
|
||||
|
||||
export interface TmdbGenre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
headquarters?: string;
|
||||
homepage?: string;
|
||||
logo_path?: string;
|
||||
origin_country?: string;
|
||||
}
|
||||
|
||||
export interface TmdbWatchProviders {
|
||||
link?: string;
|
||||
buy?: TmdbWatchProviderDetails[];
|
||||
flatrate?: TmdbWatchProviderDetails[];
|
||||
}
|
||||
|
||||
export interface TmdbWatchProviderDetails {
|
||||
display_priority?: number;
|
||||
logo_path?: string;
|
||||
provider_id: number;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user