Skip to content

Add TMDB APIs for Movie & Series. Fixes #72 #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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 @@ -121,6 +121,7 @@ Now you select the result you want and the plugin will cast it's magic and creat
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| [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 movie, 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 Key` (__not__ `API Read Access Token`) [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 @@ -150,6 +151,10 @@ Now you select the result you want and the plugin will cast it's magic and creat
- 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
131 changes: 131 additions & 0 deletions src/api/apis/TMDBMovieAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Notice, renderResults } from 'obsidian';
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';

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 searchUrl = `https://api.themoviedb.org/3/search/movie?api_key=${this.plugin.settings.TMDBKey}&query=${encodeURIComponent(title)}&include_adult=${this.plugin.settings.sfwFilter ? 'false' : 'true'}`;
const fetchData = await fetch(searchUrl);

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

const data = await fetchData.json();

if (data.total_results === 0) {
if (data.Error === 'Movie not found!') {
return [];
}

throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`);
}
if (!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,
}),
);
}

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 searchUrl = `https://api.themoviedb.org/3/movie/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
const fetchData = await fetch(searchUrl);

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

const result = await fetchData.json();
// 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,

plot: result.overview ?? '',
genres: result.genres.map((g: any) => g.name) ?? [],
writer: result.credits.crew.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [],
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 ?? 'unknown',
onlineRating: result.vote_average,
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.TMDBMovieAPI_disabledMediaTypes as MediaType[];
}
}
132 changes: 132 additions & 0 deletions src/api/apis/TMDBSeriesAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Notice, renderResults } from 'obsidian';
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';

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 searchUrl = `https://api.themoviedb.org/3/search/tv?api_key=${this.plugin.settings.TMDBKey}&query=${encodeURIComponent(title)}&include_adult=${this.plugin.settings.sfwFilter ? 'false' : 'true'}`;
const fetchData = await fetch(searchUrl);

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

const data = await fetchData.json();

if (data.total_results === 0) {
if (data.Error === 'Series not found!') {
return [];
}

throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`);
}
if (!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,
}),
);
}

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 searchUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
const fetchData = await fetch(searchUrl);

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

const result = await fetchData.json();
// 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,

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] ?? 'unknown',
onlineRating: result.vote_average,
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,

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 as MediaType[];
}
}
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
import { OMDbAPI } from './api/apis/OMDbAPI';
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
import { SteamAPI } from './api/apis/SteamAPI';
import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI';
import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI';
import { WikipediaAPI } from './api/apis/WikipediaAPI';
import { ComicVineAPI } from './api/apis/ComicVineAPI';
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
Expand Down Expand Up @@ -54,6 +56,8 @@ export default class MediaDbPlugin extends Plugin {
this.apiManager.registerAPI(new WikipediaAPI(this));
this.apiManager.registerAPI(new MusicBrainzAPI(this));
this.apiManager.registerAPI(new SteamAPI(this));
this.apiManager.registerAPI(new TMDBSeriesAPI(this));
this.apiManager.registerAPI(new TMDBMovieAPI(this));
this.apiManager.registerAPI(new BoardGameGeekAPI(this));
this.apiManager.registerAPI(new OpenLibraryAPI(this));
this.apiManager.registerAPI(new ComicVineAPI(this));
Expand Down
18 changes: 18 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { MediaType } from 'src/utils/MediaType';

export interface MediaDbPluginSettings {
OMDbKey: string;
TMDBKey: string;
MobyGamesKey: string;
GiantBombKey: string;
ComicVineKey: string;
Expand All @@ -23,6 +24,8 @@ export interface MediaDbPluginSettings {
useDefaultFrontMatter: boolean;
enableTemplaterIntegration: boolean;
OMDbAPI_disabledMediaTypes: MediaType[];
TMDBSeriesAPI_disabledMediaTypes: MediaType[];
TMDBMovieAPI_disabledMediaTypes: MediaType[];
MALAPI_disabledMediaTypes: MediaType[];
MALAPIManga_disabledMediaTypes: MediaType[];
ComicVineAPI_disabledMediaTypes: MediaType[];
Expand Down Expand Up @@ -76,6 +79,7 @@ export interface MediaDbPluginSettings {

const DEFAULT_SETTINGS: MediaDbPluginSettings = {
OMDbKey: '',
TMDBKey: '',
MobyGamesKey: '',
GiantBombKey: '',
ComicVineKey: '',
Expand All @@ -86,6 +90,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
useDefaultFrontMatter: true,
enableTemplaterIntegration: false,
OMDbAPI_disabledMediaTypes: [],
TMDBSeriesAPI_disabledMediaTypes: [],
TMDBMovieAPI_disabledMediaTypes: [],
MALAPI_disabledMediaTypes: [],
MALAPIManga_disabledMediaTypes: [],
ComicVineAPI_disabledMediaTypes: [],
Expand Down Expand Up @@ -188,6 +194,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
});
});

new Setting(containerEl)
.setName('TMDB API key')
.setDesc('API key for "www.themoviedb.org".')
.addText(cb => {
cb.setPlaceholder('API key')
.setValue(this.plugin.settings.TMDBKey)
.onChange(data => {
this.plugin.settings.TMDBKey = data;
void this.plugin.saveSettings();
});
});

new Setting(containerEl)
.setName('Moby Games key')
.setDesc('API key for "www.mobygames.com".')
Expand Down