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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ exampleVault/.obsidian/plugins/obsidian-media-db-plugin/*
!exampleVault/.obsidian/plugins/obsidian-media-db-plugin/.hotreload

exampleVault/Media DB/*
meta.txt
1 change: 1 addition & 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 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
4 changes: 2 additions & 2 deletions src/api/apis/OpenLibraryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class OpenLibraryAPI extends APIModel {
year: result.first_publish_year?.toString() ?? 'unknown',
dataSource: this.apiName,
id: result.key,
author: result.author_name.join(', '),
author: result.author_name?.join(', '),
}),
);
}
Expand Down Expand Up @@ -119,7 +119,7 @@ export class OpenLibraryAPI extends APIModel {
isbn13: Number.isNaN(isbn13) ? undefined : isbn13,
englishTitle: result.title,

author: result.author_name.join(', '),
author: result.author_name?.join(', '),
pages: Number.isNaN(pages) ? undefined : pages,
onlineRating: result.ratings_average,
image: result.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/` + result.cover_edition_key + `-L.jpg` : undefined,
Expand Down
130 changes: 130 additions & 0 deletions src/api/apis/TMDBMovieAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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[];
}
}
198 changes: 198 additions & 0 deletions src/api/apis/TMDBSeasonAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Notice, renderResults } from 'obsidian';
import type MediaDbPlugin from '../../main';
import type { MediaTypeModel } from '../../models/MediaTypeModel';
import { MediaType } from '../../utils/MediaType';
import { APIModel } from '../APIModel';
import { SeasonModel } from '../../models/SeasonModel';

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

constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'TMDBSeasonAPI';
this.apiDescription = 'A community built Series DB (seasons).';
this.apiUrl = 'https://www.themoviedb.org/';
this.types = [MediaType.Season];
this.typeMappings = new Map<string, string>();
this.typeMappings.set('tv', 'season');
}

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 searchResp = await fetch(searchUrl);

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

const searchData = await searchResp.json();
if (!searchData.results || searchData.total_results === 0) {
return [];
}

const ret: MediaTypeModel[] = [];

for (const result of searchData.results) {
if (ret.length >= 20) break;

// Fetch series details to get the total number of seasons
let totalSeasons = 0;
try {
const detailsUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(result.id)}?api_key=${this.plugin.settings.TMDBKey}`;
const detailsResp = await fetch(detailsUrl);
if (detailsResp.status === 200) {
const detailsData = await detailsResp.json();
if (Array.isArray(detailsData.seasons)) {
totalSeasons = detailsData.seasons.length;
}
}
} catch {}
ret.push(
new SeasonModel({
title: `${result.name ?? result.original_name ?? ''}`,
englishTitle: result.name ?? result.original_name ?? '',
year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
id: result.id.toString(),
seasonTitle: result.name ?? result.original_name ?? '',
seasonNumber: totalSeasons,
})
);
}

return ret;
}

//Fetch all seasons for a given series
async getSeasonsForSeries(tvId: string): Promise<SeasonModel[]> {
if (!this.plugin.settings.TMDBKey) {
throw new Error(`MDB | API key for ${this.apiName} missing.`);
}
const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}`;
const seriesResp = await fetch(seriesUrl);
if (seriesResp.status === 401) {
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
}
if (seriesResp.status !== 200) {
throw Error(`MDB | Received status code ${seriesResp.status} from ${this.apiName}.`);
}
const seriesData = await seriesResp.json();
const seriesName = seriesData?.name ?? '';
const ret: SeasonModel[] = [];
if (Array.isArray(seriesData?.seasons)) {
for (const season of seriesData.seasons) {
const seasonNumber = season.season_number ?? 0;
const titleText = `${seriesName} - Season ${seasonNumber}`;
ret.push(
new SeasonModel({
title: titleText,
englishTitle: titleText,
year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
id: `${tvId}/season/${seasonNumber}`,
seasonTitle: season.name ?? titleText,
seasonNumber: seasonNumber,
})
);
}
}
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.`);
}

// Expect season ids like "12345/season/2"
const m = /^(\d+)\/season\/(\d+)$/.exec(id);
if (!m) {
throw Error(`MDB | Invalid season id "${id}". Expected format "<tvId>/season/<seasonNumber>".`);
}

const tvId = m[1];
const seasonNumber = m[2];

const seasonUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}/season/${encodeURIComponent(seasonNumber)}?api_key=${this.plugin.settings.TMDBKey}`;
const seasonResp = await fetch(seasonUrl);

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

const seasonData = await seasonResp.json();

// Fetch parent series to build consistent titles and inherit fields
const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
const seriesResp = await fetch(seriesUrl);

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

const seriesData = await seriesResp.json();
const seriesName = seriesData?.name ?? '';

const airDate = seasonData.air_date ?? '';
const titleText = `${seriesName} - Season ${seasonData.season_number}`;

// Get airedTo as the air_date of the last episode, if available
let airedTo = 'unknown';
if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) {
const lastEp = seasonData.episodes[seasonData.episodes.length - 1];
if (lastEp?.air_date) airedTo = lastEp.air_date;
}

return new SeasonModel({
title: titleText,
englishTitle: titleText,
year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown',
dataSource: this.apiName,
url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`,
id: `${tvId}/season/${seasonData.season_number}`,
seasonTitle: seasonData.name ?? titleText,
seasonNumber: seasonData.season_number ?? Number(seasonNumber),
episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : (seasonData.episodes ?? 0),
airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown',
airedTo: airedTo,
plot: seasonData.overview ?? '',
image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '',
genres: seriesData.genres?.map((g: any) => g.name) ?? [],
writer: seriesData.created_by?.map((c: any) => c.name) ?? [],
studio: seriesData.production_companies?.map((s: any) => s.name) ?? [],
duration: seriesData.episode_run_time?.[0]?.toString() ?? '',
onlineRating: seasonData.vote_average ?? 0,
actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [],
released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status),
streamingServices: [],
airing: ['Returning Series'].includes(seriesData.status),
userData: { watched: false, lastWatched: '', personalRating: 0 },
});
}

// Settings didn’t define TMDBSeasonAPIdisabledMediaTypes yet; return an empty list for now
getDisabledMediaTypes(): MediaType[] {
return [];
}
}
Loading