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
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();
64 changes: 43 additions & 21 deletions src/api/apis/OpenLibraryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@ import { APIModel } from '../APIModel';
import type { paths } from '../schemas/OpenLibrary';

interface SearchResponse {
cover_i: number;
has_fulltext: boolean;
edition_count: number;
title: string;
author_name: string[];
first_publish_year: number;
editions: {
docs: {
key?: string;
title?: string;
cover_i?: number;
isbn?: string[];
}[];
};
cover_i?: number;
has_fulltext?: boolean;
edition_count?: number;
title?: string;
author_name?: string[];
first_publish_year?: number;
key: string;
description?: string;

number_of_pages_median?: number;
cover_edition_key?: string;
isbn?: string[];
ratings_average?: number;
}
Expand Down Expand Up @@ -69,7 +77,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 All @@ -85,8 +93,8 @@ export class OpenLibraryAPI extends APIModel {
const response = await client.GET('/search.json', {
params: {
query: {
q: `key:${id}`,
fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover_edition_key',
q: `${id}`,
fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description',
},
},
fetch: obsidianFetch,
Expand All @@ -98,31 +106,45 @@ export class OpenLibraryAPI extends APIModel {

const data = response.data as {
docs: SearchResponse[];
q?: string;
};

// TODO: maybe description.

// console.debug(data);
const result = data.docs[0];

let key = result.key;
let title = result.title;
let cover_i = result.cover_i;
let isbnArr = result.isbn;

// Check if the query is for /isbn/ or /books/ and extract from editions.docs if present
const q = data.q ?? '';
if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) {
const edition = result.editions.docs[0];
key = edition.key ?? key;
title = edition.title ?? title;
cover_i = edition.cover_i ?? cover_i;
isbnArr = edition.isbn ?? isbnArr;
}

const pages = Number(result.number_of_pages_median);
const isbn = Number((result.isbn ?? []).find((el: string) => el.length <= 10));
const isbn13 = Number((result.isbn ?? []).find((el: string) => el.length == 13));
const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10));
const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13));

return new BookModel({
title: result.title,
title: title,
year: result.first_publish_year?.toString() ?? 'unknown',
dataSource: this.apiName,
url: `https://openlibrary.org` + result.key,
id: result.key,
url: `https://openlibrary.org` + key,
id: key,
isbn: Number.isNaN(isbn) ? undefined : isbn,
isbn13: Number.isNaN(isbn13) ? undefined : isbn13,
englishTitle: result.title,
englishTitle: title,

author: result.author_name.join(', '),
author: result.author_name?.join(', '),
plot: result.description ?? undefined,
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,
image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined,

released: true,

Expand Down
158 changes: 158 additions & 0 deletions src/api/apis/TMDBMovieAPI.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 { 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.TMDBMovieAPI_disabledMediaTypes;
}
}
Loading