Skip to content

Commit e823341

Browse files
committed
Add TMDB APIs for Movie & Series. Fixes #72
1 parent c87cb09 commit e823341

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ Now you select the result you want and the plugin will cast it's magic and creat
121121
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
122122
| [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 |
123123
| [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 |
124+
| [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 |
124125
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
125126
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
126127
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No |
@@ -150,6 +151,10 @@ Now you select the result you want and the plugin will cast it's magic and creat
150151
- the ID you need is the ID of the movie or show on [IMDb](https://www.imdb.com)
151152
- you can find this ID in the URL
152153
- e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528`
154+
- [TMDB](https://www.themoviedb.org/)
155+
- the ID you need is the numeric value in the URL directly following `/movie/` or `/tv/`
156+
- e.g. for "Stargate" the URL looks like this `https://www.themoviedb.org/movie/2164-stargate` so the ID is `2164`
157+
- Please note, when searching by ID you need to select `TMDBSeriesAPI` or `TMDBMovieAPI` for series and movies respectively
153158
- [MusicBrainz](https://musicbrainz.org/)
154159
- the id of a release is not easily accessible, you are better off just searching by title
155160
- the search is generally for albums but you can have a more granular search like so:

src/api/apis/TMDBAPI.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { Notice, renderResults } from 'obsidian';
2+
import type MediaDbPlugin from '../../main';
3+
import type { MediaTypeModel } from '../../models/MediaTypeModel';
4+
import { MovieModel } from '../../models/MovieModel';
5+
import { SeriesModel } from '../../models/SeriesModel';
6+
import { MediaType } from '../../utils/MediaType';
7+
import { APIModel } from '../APIModel';
8+
9+
export class TMDBSeriesAPI extends APIModel {
10+
plugin: MediaDbPlugin;
11+
typeMappings: Map<string, string>;
12+
apiDateFormat: string = 'YYYY-MM-DD';
13+
14+
constructor(plugin: MediaDbPlugin) {
15+
super();
16+
17+
this.plugin = plugin;
18+
this.apiName = 'TMDBSeriesAPI';
19+
this.apiDescription = 'A community built Series DB.';
20+
this.apiUrl = 'https://www.themoviedb.org/';
21+
this.types = [MediaType.Series];
22+
this.typeMappings = new Map<string, string>();
23+
this.typeMappings.set('tv', 'series');
24+
}
25+
26+
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
27+
console.log(`MDB | api "${this.apiName}" queried by Title`);
28+
29+
if (!this.plugin.settings.TMDBKey) {
30+
throw new Error(`MDB | API key for ${this.apiName} missing.`);
31+
}
32+
33+
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'}`;
34+
const fetchData = await fetch(searchUrl);
35+
36+
if (fetchData.status === 401) {
37+
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
38+
}
39+
if (fetchData.status !== 200) {
40+
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
41+
}
42+
43+
const data = await fetchData.json();
44+
45+
if (data.total_results === 0) {
46+
if (data.Error === 'Series not found!') {
47+
return [];
48+
}
49+
50+
throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`);
51+
}
52+
if (!data.results) {
53+
return [];
54+
}
55+
56+
// console.debug(data.results);
57+
58+
const ret: MediaTypeModel[] = [];
59+
60+
for (const result of data.results) {
61+
ret.push(
62+
new SeriesModel({
63+
type: 'series',
64+
title: result.original_name,
65+
englishTitle: result.name,
66+
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
67+
dataSource: this.apiName,
68+
id: result.id,
69+
}),
70+
);
71+
}
72+
73+
return ret;
74+
}
75+
76+
async getById(id: string): Promise<MediaTypeModel> {
77+
console.log(`MDB | api "${this.apiName}" queried by ID`);
78+
79+
if (!this.plugin.settings.TMDBKey) {
80+
throw Error(`MDB | API key for ${this.apiName} missing.`);
81+
}
82+
83+
const searchUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
84+
const fetchData = await fetch(searchUrl);
85+
86+
if (fetchData.status === 401) {
87+
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
88+
}
89+
if (fetchData.status !== 200) {
90+
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
91+
}
92+
93+
const result = await fetchData.json();
94+
// console.debug(result);
95+
96+
return new SeriesModel({
97+
type: 'series',
98+
title: result.original_name,
99+
englishTitle: result.name,
100+
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
101+
dataSource: this.apiName,
102+
url: `https://www.themoviedb.org/tv/${result.id}`,
103+
id: result.id,
104+
105+
plot: result.overview ?? '',
106+
genres: result.genres.map((g: any) => g.name) ?? [],
107+
writer: result.created_by.map((c: any) => c.name) ?? [],
108+
studio: result.production_companies.map((s: any) => s.name) ?? [],
109+
episodes: result.number_of_episodes,
110+
duration: result.episode_run_time[0] ?? 'unknown',
111+
onlineRating: result.vote_average,
112+
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
113+
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,
114+
115+
released:['Returning Series','Cancelled','Ended'].includes(result.status),
116+
streamingServices: [],
117+
airing: ['Returning Series'].includes(result.status),
118+
airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown',
119+
airedTo: ['Returning Series'].includes(result.status) ? 'unknown' : this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown',
120+
121+
userData: {
122+
watched: false,
123+
lastWatched: '',
124+
personalRating: 0,
125+
},
126+
});
127+
128+
}
129+
130+
getDisabledMediaTypes(): MediaType[] {
131+
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes as MediaType[];
132+
}
133+
}
134+
135+
export class TMDBMovieAPI extends APIModel {
136+
plugin: MediaDbPlugin;
137+
typeMappings: Map<string, string>;
138+
apiDateFormat: string = 'YYYY-MM-DD';
139+
140+
constructor(plugin: MediaDbPlugin) {
141+
super();
142+
143+
this.plugin = plugin;
144+
this.apiName = 'TMDBMovieAPI';
145+
this.apiDescription = 'A community built Movie DB.';
146+
this.apiUrl = 'https://www.themoviedb.org/';
147+
this.types = [MediaType.Movie];
148+
this.typeMappings = new Map<string, string>();
149+
this.typeMappings.set('movie', 'movie');
150+
}
151+
152+
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
153+
console.log(`MDB | api "${this.apiName}" queried by Title`);
154+
155+
if (!this.plugin.settings.TMDBKey) {
156+
throw new Error(`MDB | API key for ${this.apiName} missing.`);
157+
}
158+
159+
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'}`;
160+
const fetchData = await fetch(searchUrl);
161+
162+
if (fetchData.status === 401) {
163+
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
164+
}
165+
if (fetchData.status !== 200) {
166+
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
167+
}
168+
169+
const data = await fetchData.json();
170+
171+
if (data.total_results === 0) {
172+
if (data.Error === 'Movie not found!') {
173+
return [];
174+
}
175+
176+
throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`);
177+
}
178+
if (!data.results) {
179+
return [];
180+
}
181+
182+
// console.debug(data.results);
183+
184+
const ret: MediaTypeModel[] = [];
185+
186+
for (const result of data.results) {
187+
ret.push(
188+
new MovieModel({
189+
type: 'movie',
190+
title: result.original_title,
191+
englishTitle: result.title,
192+
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
193+
dataSource: this.apiName,
194+
id: result.id,
195+
}),
196+
);
197+
}
198+
199+
return ret;
200+
}
201+
202+
async getById(id: string): Promise<MediaTypeModel> {
203+
console.log(`MDB | api "${this.apiName}" queried by ID`);
204+
205+
if (!this.plugin.settings.TMDBKey) {
206+
throw Error(`MDB | API key for ${this.apiName} missing.`);
207+
}
208+
209+
const searchUrl = `https://api.themoviedb.org/3/movie/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
210+
const fetchData = await fetch(searchUrl);
211+
212+
if (fetchData.status === 401) {
213+
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
214+
}
215+
if (fetchData.status !== 200) {
216+
throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`);
217+
}
218+
219+
const result = await fetchData.json();
220+
// console.debug(result);
221+
222+
return new MovieModel({
223+
type: 'movie',
224+
title: result.title,
225+
englishTitle: result.title,
226+
year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown',
227+
premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown',
228+
dataSource: this.apiName,
229+
url: `https://www.themoviedb.org/movie/${result.id}`,
230+
id: result.id,
231+
232+
plot: result.overview ?? '',
233+
genres: result.genres.map((g: any) => g.name) ?? [],
234+
writer: result.credits.crew.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [],
235+
director: result.credits.crew.filter((c: any) => c.job === 'Director').map((c: any) => c.name) ?? [],
236+
studio: result.production_companies.map((s: any) => s.name) ?? [],
237+
238+
duration: result.runtime ?? 'unknown',
239+
onlineRating: result.vote_average,
240+
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
241+
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,
242+
243+
released:['Released'].includes(result.status),
244+
streamingServices: [],
245+
246+
userData: {
247+
watched: false,
248+
lastWatched: '',
249+
personalRating: 0,
250+
},
251+
});
252+
253+
}
254+
255+
getDisabledMediaTypes(): MediaType[] {
256+
return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes as MediaType[];
257+
}
258+
}

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
1111
import { OMDbAPI } from './api/apis/OMDbAPI';
1212
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
1313
import { SteamAPI } from './api/apis/SteamAPI';
14+
import { TMDBSeriesAPI } from './api/apis/TMDBAPI';
15+
import { TMDBMovieAPI } from './api/apis/TMDBAPI';
1416
import { WikipediaAPI } from './api/apis/WikipediaAPI';
1517
import { ComicVineAPI } from './api/apis/ComicVineAPI';
1618
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
@@ -54,6 +56,8 @@ export default class MediaDbPlugin extends Plugin {
5456
this.apiManager.registerAPI(new WikipediaAPI(this));
5557
this.apiManager.registerAPI(new MusicBrainzAPI(this));
5658
this.apiManager.registerAPI(new SteamAPI(this));
59+
this.apiManager.registerAPI(new TMDBSeriesAPI(this));
60+
this.apiManager.registerAPI(new TMDBMovieAPI(this));
5761
this.apiManager.registerAPI(new BoardGameGeekAPI(this));
5862
this.apiManager.registerAPI(new OpenLibraryAPI(this));
5963
this.apiManager.registerAPI(new ComicVineAPI(this));

src/settings/Settings.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { MediaType } from 'src/utils/MediaType';
1313

1414
export interface MediaDbPluginSettings {
1515
OMDbKey: string;
16+
TMDBKey: string;
1617
MobyGamesKey: string;
1718
GiantBombKey: string;
1819
ComicVineKey: string;
@@ -23,6 +24,8 @@ export interface MediaDbPluginSettings {
2324
useDefaultFrontMatter: boolean;
2425
enableTemplaterIntegration: boolean;
2526
OMDbAPI_disabledMediaTypes: MediaType[];
27+
TMDBSeriesAPI_disabledMediaTypes: MediaType[];
28+
TMDBMovieAPI_disabledMediaTypes: MediaType[];
2629
MALAPI_disabledMediaTypes: MediaType[];
2730
MALAPIManga_disabledMediaTypes: MediaType[];
2831
ComicVineAPI_disabledMediaTypes: MediaType[];
@@ -76,6 +79,7 @@ export interface MediaDbPluginSettings {
7679

7780
const DEFAULT_SETTINGS: MediaDbPluginSettings = {
7881
OMDbKey: '',
82+
TMDBKey: '',
7983
MobyGamesKey: '',
8084
GiantBombKey: '',
8185
ComicVineKey: '',
@@ -86,6 +90,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
8690
useDefaultFrontMatter: true,
8791
enableTemplaterIntegration: false,
8892
OMDbAPI_disabledMediaTypes: [],
93+
TMDBSeriesAPI_disabledMediaTypes: [],
94+
TMDBMovieAPI_disabledMediaTypes: [],
8995
MALAPI_disabledMediaTypes: [],
9096
MALAPIManga_disabledMediaTypes: [],
9197
ComicVineAPI_disabledMediaTypes: [],
@@ -188,6 +194,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
188194
});
189195
});
190196

197+
new Setting(containerEl)
198+
.setName('TMDB API key')
199+
.setDesc('API key for "www.themoviedb.org".')
200+
.addText(cb => {
201+
cb.setPlaceholder('API key')
202+
.setValue(this.plugin.settings.TMDBKey)
203+
.onChange(data => {
204+
this.plugin.settings.TMDBKey = data;
205+
void this.plugin.saveSettings();
206+
});
207+
});
208+
191209
new Setting(containerEl)
192210
.setName('Moby Games key')
193211
.setDesc('API key for "www.mobygames.com".')

0 commit comments

Comments
 (0)