Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Now you select the result you want, and the plugin will cast its magic, creating
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes |
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movies, series, and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (__not__ `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes |
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all Steam games. | games | No | 10000 per day | No |
Expand Down Expand Up @@ -148,6 +149,10 @@ Now you select the result you want, and the plugin will cast its magic, creating
- the ID you need is the ID of the movie or show on [IMDb](https://www.imdb.com)
- you can find this ID in the URL
- e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528`
- [TMDB](https://www.themoviedb.org/)
- the ID you need is the numeric value in the URL directly following `/movie/` or `/tv/`
- e.g. for "Stargate" the URL looks like this `https://www.themoviedb.org/movie/2164-stargate` so the ID is `2164`
- Please note, when searching by ID you need to select `TMDBSeriesAPI` or `TMDBMovieAPI` for series and movies respectively
- [MusicBrainz](https://musicbrainz.org/)
- the id of a release is not easily accessible; you are better off just searching by title
- the search is generally for albums but you can have a more granular search like so:
Expand Down
3 changes: 3 additions & 0 deletions automation/fetchSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ async function fetchSchema() {

// https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml
await $('bun openapi-typescript ./src/api/schemas/OpenLibrary.json -o ./src/api/schemas/OpenLibrary.ts');

// https://developer.themoviedb.org/openapi
await $('bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./src/api/schemas/TMDB.ts')
}

await fetchSchema();
159 changes: 159 additions & 0 deletions src/api/apis/TMDBMovieAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import createClient from 'openapi-fetch';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MovieModel } from '../../models/MovieModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
import type { paths } from '../schemas/TMDB';

export class TMDBMovieAPI extends APIModel {
plugin: MediaDbPlugin;
typeMappings: Map<string, string>;
apiDateFormat: string = 'YYYY-MM-DD';

constructor(plugin: MediaDbPlugin) {
super();

this.plugin = plugin;
this.apiName = 'TMDBMovieAPI';
this.apiDescription = 'A community built Movie DB.';
this.apiUrl = 'https://www.themoviedb.org/';
this.types = [MediaType.Movie];
this.typeMappings = new Map<string, string>();
this.typeMappings.set('movie', 'movie');
}

async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);

if (!this.plugin.settings.TMDBKey) {
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}

const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
const response = await client.GET('/3/search/movie', {
headers: {
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`
},
params: {
query: {
query: encodeURIComponent(title),
include_adult: this.plugin.settings.sfwFilter ? false : true
},
},
fetch: fetch,
});

if (response.response.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}

const data = response.data

if(!data) {
throw Error(`MDB | No data received from ${this.apiName}.`);
}

if ( data.total_results === 0 || !data.results ) {
return [];
}

// console.debug(data.results);

const ret: MediaTypeModel[] = [];

for (const result of data.results) {
ret.push(
new MovieModel({
type: 'movie',
title: result.original_title,
englishTitle: result.title,
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
id: result.id.toString(),
}),
);
}

return ret;
}

async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);

if (!this.plugin.settings.TMDBKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}

const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
const response = await client.GET('/3/movie/{movie_id}', {
headers: {
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`
},
params: {
path: { movie_id: parseInt(id) },
query: {
append_to_response: 'credits'
},
},
fetch: fetch,
});

if (response.response.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}

const result = response.data

if (!result) {
throw Error(`MDB | No data received from ${this.apiName}.`);
}
// console.debug(result);

return new MovieModel({
type: 'movie',
title: result.title,
englishTitle: result.title,
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown',
dataSource: this.apiName,
url: `https://www.themoviedb.org/movie/${result.id}`,
id: result.id.toString(),

plot: result.overview ?? '',
genres: result.genres?.map((g: any) => g.name) ?? [],
// TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type
// @ts-ignore
writer: result.credits.crew?.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [],
// @ts-ignore
director: result.credits.crew?.filter((c: any) => c.job === 'Director').map((c: any) => c.name) ?? [],
studio: result.production_companies?.map((s: any) => s.name) ?? [],

duration: result.runtime?.toString() ?? 'unknown',
onlineRating: result.vote_average,
// @ts-ignore
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,

released:['Released'].includes(result.status!),
streamingServices: [],

userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
});

}

getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes;
}
}
158 changes: 158 additions & 0 deletions src/api/apis/TMDBSeriesAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import createClient from 'openapi-fetch';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { SeriesModel } from '../../models/SeriesModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
import type { paths } from '../schemas/TMDB';

export class TMDBSeriesAPI extends APIModel {
plugin: MediaDbPlugin;
typeMappings: Map<string, string>;
apiDateFormat: string = 'YYYY-MM-DD';

constructor(plugin: MediaDbPlugin) {
super();

this.plugin = plugin;
this.apiName = 'TMDBSeriesAPI';
this.apiDescription = 'A community built Series DB.';
this.apiUrl = 'https://www.themoviedb.org/';
this.types = [MediaType.Series];
this.typeMappings = new Map<string, string>();
this.typeMappings.set('tv', 'series');
}

async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);

if (!this.plugin.settings.TMDBKey) {
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}

const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
const response = await client.GET('/3/search/tv', {
headers: {
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`
},
params: {
query: {
query: encodeURIComponent(title),
include_adult: this.plugin.settings.sfwFilter ? false : true
},
},
fetch: fetch,
});

if (response.response.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}

const data = response.data

if(!data) {
throw Error(`MDB | No data received from ${this.apiName}.`);
}

if ( data.total_results === 0 || !data.results ) {
return [];
}

// console.debug(data.results);

const ret: MediaTypeModel[] = [];

for (const result of data.results) {
ret.push(
new SeriesModel({
type: 'series',
title: result.original_name,
englishTitle: result.name,
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
id: result.id.toString(),
}),
);
}

return ret;
}

async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);

if (!this.plugin.settings.TMDBKey) {
throw Error(`MDB | API key for ${this.apiName} missing.`);
}

const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
const response = await client.GET('/3/tv/{series_id}', {
headers: {
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`
},
params: {
path: { series_id: parseInt(id) },
query: {
append_to_response: 'credits'
},
},
fetch: fetch,
});

if (response.response.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (response.response.status !== 200) {
throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`);
}

const result = response.data

if (!result) {
throw Error(`MDB | No data received from ${this.apiName}.`);
}
// console.debug(result);

return new SeriesModel({
type: 'series',
title: result.original_name,
englishTitle: result.name,
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
url: `https://www.themoviedb.org/tv/${result.id}`,
id: result.id.toString(),

plot: result.overview ?? '',
genres: result.genres?.map((g: any) => g.name) ?? [],
writer: result.created_by?.map((c: any) => c.name) ?? [],
studio: result.production_companies?.map((s: any) => s.name) ?? [],
episodes: result.number_of_episodes,
duration: result.episode_run_time?.[0]?.toString() ?? 'unknown',
onlineRating: result.vote_average,
// TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type
// @ts-ignore
actors: result.credits?.cast.map((c: any) => c.name).slice(0, 5) ?? [],
image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null,

released:['Returning Series','Cancelled','Ended'].includes(result.status!),
streamingServices: [],
airing: ['Returning Series'].includes(result.status!),
airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown',
airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown',

userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
});

}

getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes;
}
}
Loading