migrate to secret storage and other improvements
This commit is contained in:
parent
faeca3459c
commit
7d6722c38e
28 changed files with 415 additions and 292 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
28
package.json
28
package.json
|
|
@ -22,27 +22,25 @@
|
|||
"author": "Moritz Jung",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^18.0.1",
|
||||
"@lemons_dev/parsinom": "^0.0.12",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/bun": "^1.3.7",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"eslint": "^9.39.2",
|
||||
"@happy-dom/global-registrator": "^20.8.9",
|
||||
"@lemons_dev/parsinom": "^0.1.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-only-warn": "^1.2.1",
|
||||
"iso-639-2": "^3.0.2",
|
||||
"obsidian": "latest",
|
||||
"openapi-fetch": "^0.14.1",
|
||||
"openapi-typescript": "^7.10.1",
|
||||
"openapi-fetch": "^0.17.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"prettier": "^3.8.1",
|
||||
"solid-js": "^1.9.3",
|
||||
"solid-js": "^1.9.12",
|
||||
"string-argv": "^0.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^6.0.5",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.5",
|
||||
"vite-plugin-banner": "^0.8.1",
|
||||
"vite-plugin-solid": "^2.11.0",
|
||||
"vite-plugin-static-copy": "^3.2.0"
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"vite-plugin-static-copy": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class APIManager {
|
|||
console.debug(`MDB | api manager queried with "${query}"`);
|
||||
|
||||
const promises = this.apis
|
||||
.filter(api => apisToQuery.contains(api.apiName))
|
||||
.filter(api => apisToQuery.includes(api.apiName))
|
||||
.map(async api => {
|
||||
try {
|
||||
return await api.searchByTitle(query);
|
||||
|
|
@ -53,7 +53,7 @@ export class APIManager {
|
|||
for (const api of this.apis) {
|
||||
if (api.apiName === apiName) {
|
||||
try {
|
||||
return api.getById(id);
|
||||
return await api.getById(id);
|
||||
} catch (e) {
|
||||
new Notice(`Error querying ${api.apiName}: ${e}`);
|
||||
console.warn(e);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { requestUrl } from 'obsidian';
|
||||
import { BoardGameModel } from 'src/models/BoardGameModel';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { BoardGameModel } from '../../models/BoardGameModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
|
|
@ -22,12 +22,16 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId);
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -67,12 +71,16 @@ export class BoardGameGeekAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId);
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { requestUrl } from 'obsidian';
|
||||
import { ComicMangaModel } from 'src/models/ComicMangaModel';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { ComicMangaModel } from '../../models/ComicMangaModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { APIModel } from '../APIModel';
|
||||
|
|
@ -24,8 +24,12 @@ export class ComicVineAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId);
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/search/?api_key=${this.plugin.settings.ComicVineKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`;
|
||||
const searchUrl = `${this.apiUrl}/search/?api_key=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
|
|
@ -55,8 +59,12 @@ export class ComicVineAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId);
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.ComicVineKey}&format=json`;
|
||||
const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${key}&format=json`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import { obsidianFetch } from 'src/utils/Utils';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { GameModel } from '../../models/GameModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { obsidianFetch } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/GiantBomb';
|
||||
|
||||
|
|
@ -23,8 +23,9 @@ export class GiantBombAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId);
|
||||
|
||||
if (!this.plugin.settings.GiantBombKey) {
|
||||
if (!key) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ export class GiantBombAPI extends APIModel {
|
|||
const response = await client.GET('/games', {
|
||||
params: {
|
||||
query: {
|
||||
api_key: this.plugin.settings.GiantBombKey,
|
||||
api_key: key,
|
||||
filter: `name:${title}`,
|
||||
format: 'json',
|
||||
limit: 20,
|
||||
|
|
@ -73,8 +74,9 @@ export class GiantBombAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.GiantBombKeyId);
|
||||
|
||||
if (!this.plugin.settings.GiantBombKey) {
|
||||
if (!key) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +87,7 @@ export class GiantBombAPI extends APIModel {
|
|||
guid: id,
|
||||
},
|
||||
query: {
|
||||
api_key: this.plugin.settings.GiantBombKey,
|
||||
api_key: key,
|
||||
format: 'json',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import { isTruthy, obsidianFetch } from 'src/utils/Utils';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MovieModel } from '../../models/MovieModel';
|
||||
import { SeriesModel } from '../../models/SeriesModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { isTruthy, obsidianFetch } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/MALAPI';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import { isTruthy, obsidianFetch } from 'src/utils/Utils';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { ComicMangaModel } from '../../models/ComicMangaModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { isTruthy, obsidianFetch } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/MALAPI';
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,13 @@ export class MobyGamesAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.MobyGamesKeyId);
|
||||
|
||||
if (!this.plugin.settings.MobyGamesKey) {
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`;
|
||||
const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${key}`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
|
|
@ -70,12 +71,13 @@ export class MobyGamesAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.MobyGamesKeyId);
|
||||
|
||||
if (!this.plugin.settings.MobyGamesKey) {
|
||||
if (!key) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${this.plugin.settings.MobyGamesKey}`;
|
||||
const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${key}`;
|
||||
const fetchData = await requestUrl({
|
||||
url: searchUrl,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,13 +76,14 @@ export class OMDbAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId);
|
||||
|
||||
if (!this.plugin.settings.OMDbKey) {
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`,
|
||||
url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
|
|
@ -160,13 +161,14 @@ export class OMDbAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId);
|
||||
|
||||
if (!this.plugin.settings.OMDbKey) {
|
||||
if (!key) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`,
|
||||
url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import createClient from 'openapi-fetch';
|
||||
import { BookModel } from 'src/models/BookModel';
|
||||
import { obsidianFetch } from 'src/utils/Utils';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import { BookModel } from '../../models/BookModel';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
import { MediaType } from '../../utils/MediaType';
|
||||
import { obsidianFetch } from '../../utils/Utils';
|
||||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/OpenLibrary';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
||||
|
||||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
|
|
@ -8,6 +6,36 @@ import { MediaType } from '../../utils/MediaType';
|
|||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
interface TMDBCreditMember {
|
||||
name?: string | null;
|
||||
job?: string | null;
|
||||
}
|
||||
|
||||
interface TMDBCreditsResponse {
|
||||
credits?: {
|
||||
cast?: TMDBCreditMember[];
|
||||
crew?: TMDBCreditMember[];
|
||||
};
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] {
|
||||
return (credits?.cast ?? [])
|
||||
.map(c => c.name)
|
||||
.filter(isNonEmptyString)
|
||||
.slice(0, size);
|
||||
}
|
||||
|
||||
function getCrewNamesByJob(credits: TMDBCreditsResponse['credits'], job: string): string[] {
|
||||
return (credits?.crew ?? [])
|
||||
.filter(c => c.job === job)
|
||||
.map(c => c.name)
|
||||
.filter(isNonEmptyString);
|
||||
}
|
||||
|
||||
export class TMDBMovieAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
|
|
@ -27,15 +55,16 @@ export class TMDBMovieAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
if (!key) {
|
||||
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}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
|
|
@ -85,15 +114,16 @@ export class TMDBMovieAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
if (!key) {
|
||||
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}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: { movie_id: parseInt(id) },
|
||||
|
|
@ -117,6 +147,7 @@ export class TMDBMovieAPI extends APIModel {
|
|||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
// console.debug(result);
|
||||
const credits = (result as TMDBCreditsResponse).credits;
|
||||
|
||||
return new MovieModel({
|
||||
type: 'movie',
|
||||
|
|
@ -129,18 +160,14 @@ export class TMDBMovieAPI extends APIModel {
|
|||
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) ?? [],
|
||||
genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [],
|
||||
writer: getCrewNamesByJob(credits, 'Screenplay'),
|
||||
director: getCrewNamesByJob(credits, 'Director'),
|
||||
studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [],
|
||||
|
||||
duration: result.runtime?.toString() ?? 'unknown',
|
||||
onlineRating: result.vote_average,
|
||||
// @ts-ignore
|
||||
actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [],
|
||||
actors: getTopCastNames(credits, 5),
|
||||
image: `https://image.tmdb.org/t/p/w780${result.poster_path}`,
|
||||
|
||||
released: ['Released'].includes(result.status!),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
||||
|
||||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
|
|
@ -8,6 +6,40 @@ import { MediaType } from '../../utils/MediaType';
|
|||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
interface NamedEntity {
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
interface CastMember {
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
interface CreditsLike {
|
||||
cast?: CastMember[] | null;
|
||||
}
|
||||
|
||||
function extractNames(items: (NamedEntity | null | undefined)[] | null | undefined): string[] {
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map(item => item?.name?.trim() ?? '').filter(name => name.length > 0);
|
||||
}
|
||||
|
||||
function getTopActorNames(credits: CreditsLike | null | undefined, limit: number = 5): string[] {
|
||||
if (!credits || !Array.isArray(credits.cast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return credits.cast
|
||||
.map(member => {
|
||||
const name = member?.name;
|
||||
return typeof name === 'string' ? name : '';
|
||||
})
|
||||
.filter(name => name.length > 0)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
export class TMDBSeasonAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
|
|
@ -27,15 +59,16 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
|
||||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const searchResponse = await client.GET('/3/search/tv', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
|
|
@ -60,36 +93,32 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
return [];
|
||||
}
|
||||
|
||||
const ret: MediaTypeModel[] = [];
|
||||
const topResults = searchData.results.slice(0, 20);
|
||||
|
||||
for (const result of searchData.results) {
|
||||
if (ret.length >= 20) break;
|
||||
|
||||
// Fetch series details to get the total number of seasons
|
||||
return await Promise.all(
|
||||
topResults.map(async result => {
|
||||
let totalSeasons = 0;
|
||||
if (typeof result.id === 'number') {
|
||||
try {
|
||||
const detailsResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: result.id ?? 0 },
|
||||
path: { series_id: result.id },
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
||||
if (detailsResponse.response.status === 200 && detailsResponse.data) {
|
||||
const detailsData = detailsResponse.data;
|
||||
if (Array.isArray(detailsData.seasons)) {
|
||||
totalSeasons = detailsData.seasons.length;
|
||||
}
|
||||
if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) {
|
||||
totalSeasons = detailsResponse.data.seasons.length;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors and assume 0 seasons
|
||||
// Ignore detail errors and use 0 as fallback.
|
||||
}
|
||||
}
|
||||
|
||||
ret.push(
|
||||
new SeasonModel({
|
||||
return 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',
|
||||
|
|
@ -97,26 +126,25 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
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) {
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
if (!key) {
|
||||
throw new Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
const seriesResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(tvId) },
|
||||
path: { series_id: Number.parseInt(tvId, 10) },
|
||||
},
|
||||
fetch: fetch,
|
||||
});
|
||||
|
|
@ -158,8 +186,9 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
if (!key) {
|
||||
throw Error(`MDB | API key for ${this.apiName} missing.`);
|
||||
}
|
||||
|
||||
|
|
@ -169,20 +198,20 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
throw Error(`MDB | Invalid season id "${id}". Expected format "<series_id>/season/<season_number>".`);
|
||||
}
|
||||
|
||||
const tvId = m[1];
|
||||
const seasonNumber = m[2];
|
||||
const tvId = Number.parseInt(m[1], 10);
|
||||
const seasonNumber = Number.parseInt(m[2], 10);
|
||||
|
||||
const client = createClient<paths>({ baseUrl: 'https://api.themoviedb.org' });
|
||||
|
||||
// Fetch season details
|
||||
const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: {
|
||||
series_id: parseInt(tvId),
|
||||
season_number: parseInt(seasonNumber),
|
||||
series_id: tvId,
|
||||
season_number: seasonNumber,
|
||||
},
|
||||
},
|
||||
fetch: fetch,
|
||||
|
|
@ -203,10 +232,10 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
// Fetch parent series to build consistent titles and inherit fields
|
||||
const seriesResponse = await client.GET('/3/tv/{series_id}', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.plugin.settings.TMDBKey}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(tvId) },
|
||||
path: { series_id: tvId },
|
||||
query: {
|
||||
append_to_response: 'credits',
|
||||
},
|
||||
|
|
@ -238,28 +267,28 @@ export class TMDBSeasonAPI extends APIModel {
|
|||
const lastEp = seasonData.episodes[seasonData.episodes.length - 1];
|
||||
if (lastEp?.air_date) airedTo = lastEp.air_date;
|
||||
}
|
||||
const formattedAiredTo = airedTo === 'unknown' ? 'unknown' : (this.plugin.dateFormatter.format(airedTo, this.apiDateFormat) ?? airedTo);
|
||||
|
||||
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}`,
|
||||
url: `https://www.themoviedb.org/tv/${tvId.toString()}/season/${seasonData.season_number}`,
|
||||
id: `${tvId.toString()}/season/${seasonData.season_number}`,
|
||||
seasonTitle: seasonData.name ?? titleText,
|
||||
seasonNumber: seasonData.season_number ?? Number(seasonNumber),
|
||||
seasonNumber: seasonData.season_number ?? seasonNumber,
|
||||
episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0,
|
||||
airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown',
|
||||
airedTo: airedTo,
|
||||
airedTo: formattedAiredTo,
|
||||
plot: seasonData.overview ?? '',
|
||||
image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '',
|
||||
genres: seriesData.genres?.map(g => g.name ?? '').filter(name => name !== '') ?? [],
|
||||
writer: seriesData.created_by?.map(c => c.name ?? '').filter(name => name !== '') ?? [],
|
||||
studio: seriesData.production_companies?.map(s => s.name ?? '').filter(name => name !== '') ?? [],
|
||||
genres: extractNames(seriesData.genres),
|
||||
writer: extractNames(seriesData.created_by),
|
||||
studio: extractNames(seriesData.production_companies),
|
||||
duration: seriesData.episode_run_time?.[0]?.toString() ?? '',
|
||||
onlineRating: seasonData.vote_average ?? 0,
|
||||
// @ts-ignore - append_to_response credits not reflected in base schema
|
||||
actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [],
|
||||
actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits),
|
||||
released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''),
|
||||
streamingServices: [],
|
||||
airing: ['Returning Series'].includes(seriesData.status ?? ''),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
||||
|
||||
import createClient from 'openapi-fetch';
|
||||
import type MediaDbPlugin from '../../main';
|
||||
import type { MediaTypeModel } from '../../models/MediaTypeModel';
|
||||
|
|
@ -8,6 +6,27 @@ import { MediaType } from '../../utils/MediaType';
|
|||
import { APIModel } from '../APIModel';
|
||||
import type { paths } from '../schemas/TMDB';
|
||||
|
||||
interface TMDBCreditMember {
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
interface TMDBCreditsResponse {
|
||||
credits?: {
|
||||
cast?: TMDBCreditMember[];
|
||||
};
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
function getTopCastNames(credits: TMDBCreditsResponse['credits'], size: number): string[] {
|
||||
return (credits?.cast ?? [])
|
||||
.map(c => c.name)
|
||||
.filter(isNonEmptyString)
|
||||
.slice(0, size);
|
||||
}
|
||||
|
||||
export class TMDBSeriesAPI extends APIModel {
|
||||
plugin: MediaDbPlugin;
|
||||
typeMappings: Map<string, string>;
|
||||
|
|
@ -28,14 +47,15 @@ export class TMDBSeriesAPI extends APIModel {
|
|||
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by Title`);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
if (!key) {
|
||||
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}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
|
|
@ -85,15 +105,16 @@ export class TMDBSeriesAPI extends APIModel {
|
|||
|
||||
async getById(id: string): Promise<MediaTypeModel> {
|
||||
console.log(`MDB | api "${this.apiName}" queried by ID`);
|
||||
const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId);
|
||||
|
||||
if (!this.plugin.settings.TMDBKey) {
|
||||
if (!key) {
|
||||
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}`,
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
params: {
|
||||
path: { series_id: parseInt(id) },
|
||||
|
|
@ -117,6 +138,7 @@ export class TMDBSeriesAPI extends APIModel {
|
|||
throw Error(`MDB | No data received from ${this.apiName}.`);
|
||||
}
|
||||
// console.debug(result);
|
||||
const credits = (result as TMDBCreditsResponse).credits;
|
||||
|
||||
return new SeriesModel({
|
||||
type: 'series',
|
||||
|
|
@ -128,15 +150,13 @@ export class TMDBSeriesAPI extends APIModel {
|
|||
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) ?? [],
|
||||
genres: result.genres?.map(g => g.name).filter(isNonEmptyString) ?? [],
|
||||
writer: result.created_by?.map(c => c.name).filter(isNonEmptyString) ?? [],
|
||||
studio: result.production_companies?.map(s => s.name).filter(isNonEmptyString) ?? [],
|
||||
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) ?? [],
|
||||
actors: getTopCastNames(credits, 5),
|
||||
image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null,
|
||||
|
||||
released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status!),
|
||||
|
|
|
|||
38
src/main.ts
38
src/main.ts
|
|
@ -1,7 +1,6 @@
|
|||
import type { TFile } from 'obsidian';
|
||||
import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian';
|
||||
import { requestUrl, normalizePath } from 'obsidian';
|
||||
import type { MediaType } from 'src/utils/MediaType';
|
||||
import { APIManager } from './api/APIManager';
|
||||
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
|
||||
import { ComicVineAPI } from './api/apis/ComicVineAPI';
|
||||
|
|
@ -29,6 +28,7 @@ import type { MediaDbPluginSettings } from './settings/Settings';
|
|||
import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings';
|
||||
import { BulkImportHelper } from './utils/BulkImportHelper';
|
||||
import { DateFormatter } from './utils/DateFormatter';
|
||||
import type { MediaType } from './utils/MediaType';
|
||||
import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager';
|
||||
import type { SearchModalOptions } from './utils/ModalHelper';
|
||||
import { ModalHelper } from './utils/ModalHelper';
|
||||
|
|
@ -218,7 +218,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
|
||||
// filter the results
|
||||
apiSearchResults = apiSearchResults.filter(x => types.contains(x.type));
|
||||
apiSearchResults = apiSearchResults.filter(x => types.includes(x.type));
|
||||
|
||||
if (apiSearchResults.length === 0) {
|
||||
new Notice('No results found for the selected types.');
|
||||
|
|
@ -373,15 +373,11 @@ export default class MediaDbPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
let selectResults: MediaTypeModel[];
|
||||
const proceed: boolean = false;
|
||||
|
||||
while (!proceed) {
|
||||
selectResults =
|
||||
const selectResults =
|
||||
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
||||
return await this.queryDetails(selectModalData.selected);
|
||||
})) ?? [];
|
||||
if (!selectResults || selectResults.length < 1) {
|
||||
if (selectResults.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -391,10 +387,8 @@ export default class MediaDbPlugin extends Plugin {
|
|||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.createMediaDbNotes(selectResults!);
|
||||
await this.createMediaDbNotes(selectResults);
|
||||
}
|
||||
|
||||
async createEntryWithIdSearchModal(): Promise<void> {
|
||||
|
|
@ -556,8 +550,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach);
|
||||
// TODO: better object merging
|
||||
fileMetadata = Object.assign(attachFileMetadata, fileMetadata);
|
||||
fileMetadata = { ...attachFileMetadata, ...fileMetadata };
|
||||
|
||||
let attachFileContent: string = await this.app.vault.read(fileToAttach);
|
||||
const regExp = new RegExp(this.frontMatterRexExpPattern);
|
||||
|
|
@ -574,8 +567,7 @@ export default class MediaDbPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const templateMetadata = this.getMetaDataFromFileContent(template);
|
||||
// TODO: better object merging
|
||||
fileMetadata = Object.assign(templateMetadata, fileMetadata);
|
||||
fileMetadata = { ...templateMetadata, ...fileMetadata };
|
||||
|
||||
const regExp = new RegExp(this.frontMatterRexExpPattern);
|
||||
const attachFileContent = template.replace(regExp, '');
|
||||
|
|
@ -709,6 +701,20 @@ export default class MediaDbPlugin extends Plugin {
|
|||
const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this);
|
||||
const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings);
|
||||
|
||||
// delete old api keys
|
||||
// @ts-ignore
|
||||
delete loadedSettings.BoardgameGeekKey;
|
||||
// @ts-ignore
|
||||
delete loadedSettings.ComicVineKey;
|
||||
// @ts-ignore
|
||||
delete loadedSettings.GiantBombKey;
|
||||
// @ts-ignore
|
||||
delete loadedSettings.MobyGamesKey;
|
||||
// @ts-ignore
|
||||
delete loadedSettings.OMDbKey;
|
||||
// @ts-ignore
|
||||
delete loadedSettings.TMDBKey;
|
||||
|
||||
// Migrate property mappings using the dedicated migration method
|
||||
const migratedModels = PropertyMappingModel.migrateModels(
|
||||
loadedSettings.propertyMappingModels || [],
|
||||
|
|
@ -719,6 +725,8 @@ export default class MediaDbPlugin extends Plugin {
|
|||
loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON());
|
||||
|
||||
this.settings = loadedSettings;
|
||||
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ButtonComponent } from 'obsidian';
|
||||
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
|
||||
import type { APIModel } from 'src/api/APIModel';
|
||||
import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper';
|
||||
import type { APIModel } from '../api/APIModel';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import { BulkImportLookupMethod } from '../utils/BulkImportHelper';
|
||||
|
||||
export class MediaDbBulkImportModal extends Modal {
|
||||
plugin: MediaDbPlugin;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian';
|
||||
import type MediaDbPlugin from 'src/main';
|
||||
import type { MediaTypeModel } from 'src/models/MediaTypeModel';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper';
|
||||
import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper';
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class MediaDbSearchModal extends Modal {
|
|||
|
||||
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
|
||||
apiToggleComponent.setTooltip(unCamelCase(mediaType));
|
||||
apiToggleComponent.setValue(this.selectedTypes.contains(mediaType));
|
||||
apiToggleComponent.setValue(this.selectedTypes.includes(mediaType));
|
||||
if (apiToggleComponent.getValue()) {
|
||||
currentToggle = apiToggleComponent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MediaType } from 'src/utils/MediaType';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaType } from '../utils/MediaType';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import { PropertyMappingOption } from './PropertyMapping';
|
||||
|
||||
|
|
@ -21,9 +21,7 @@ export class PropertyMapper {
|
|||
return obj;
|
||||
}
|
||||
|
||||
// console.log(obj.type);
|
||||
|
||||
if (MEDIA_TYPES.filter(x => x.toString() == obj.type).length < 1) {
|
||||
if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -33,33 +31,32 @@ export class PropertyMapper {
|
|||
}
|
||||
|
||||
const propertyMappings = propertyMappingModel.properties;
|
||||
const propertyMappingByProperty = new Map(propertyMappings.map(mapping => [mapping.property, mapping]));
|
||||
|
||||
const newObj: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.property === key) {
|
||||
const propertyMapping = propertyMappingByProperty.get(key);
|
||||
|
||||
if (!propertyMapping) {
|
||||
newObj[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
let finalValue = value;
|
||||
if (propertyMapping.wikilink) {
|
||||
if (typeof value === 'string') {
|
||||
finalValue = `[[${value}]]`;
|
||||
} else if (Array.isArray(value)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
finalValue = value.map(v => (typeof v === 'string' ? `[[${v}]]` : v));
|
||||
finalValue = (value as unknown[]).map((v: unknown) => (typeof v === 'string' ? `[[${v}]]` : v));
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyMapping.mapping === PropertyMappingOption.Map) {
|
||||
// @ts-ignore
|
||||
newObj[propertyMapping.newProperty] = finalValue;
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Remove) {
|
||||
// do nothing
|
||||
} else if (propertyMapping.mapping === PropertyMappingOption.Default) {
|
||||
// @ts-ignore
|
||||
newObj[key] = finalValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newObj;
|
||||
|
|
@ -80,34 +77,28 @@ export class PropertyMapper {
|
|||
obj.type = 'comicManga';
|
||||
console.debug(`MDB | updated metadata type`, obj.type);
|
||||
}
|
||||
if (MEDIA_TYPES.contains(obj.type as MediaType)) {
|
||||
if (!MEDIA_TYPES.includes(obj.type as MediaType)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type);
|
||||
const propertyMappings = propertyMappingModel?.properties ?? [];
|
||||
const propertyMappingByOriginal = new Map(propertyMappings.map(mapping => [mapping.property, mapping]));
|
||||
const propertyMappingByMapped = new Map(propertyMappings.map(mapping => [mapping.newProperty, mapping]));
|
||||
|
||||
const originalObj: Record<string, unknown> = {};
|
||||
const originalObj: Record<string, unknown> = { ...obj };
|
||||
|
||||
objLoop: for (const [key, value] of Object.entries(obj)) {
|
||||
// first try if it is a normal property
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.property === key) {
|
||||
// @ts-ignore
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const normalProperty = propertyMappingByOriginal.get(key);
|
||||
if (normalProperty) {
|
||||
originalObj[key] = value;
|
||||
|
||||
continue objLoop;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise see if it is a mapped property
|
||||
for (const propertyMapping of propertyMappings) {
|
||||
if (propertyMapping.newProperty === key) {
|
||||
// @ts-ignore
|
||||
originalObj[propertyMapping.property] = value;
|
||||
|
||||
continue objLoop;
|
||||
}
|
||||
const mappedProperty = propertyMappingByMapped.get(key);
|
||||
if (mappedProperty) {
|
||||
originalObj[mappedProperty.property] = value;
|
||||
delete originalObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode
|
|||
<td class="col-new-name">
|
||||
<Show
|
||||
when={property.mapping === PropertyMappingOption.Map}
|
||||
fallback={<span class="media-db-plugin-property-mapping-to-disabled">—</span>}
|
||||
fallback={<span class="media-db-plugin-property-mapping-to-disabled">N/A</span>}
|
||||
>
|
||||
<div class="media-db-plugin-property-mapping-to">
|
||||
<Icon iconName="arrow-right" />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, PluginSettingTab, SettingGroup } from 'obsidian';
|
||||
import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian';
|
||||
import { render } from 'solid-js/web';
|
||||
import { MediaType } from 'src/utils/MediaType';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import { MediaType } from '../utils/MediaType';
|
||||
import { MEDIA_TYPES } from '../utils/MediaTypeManager';
|
||||
import { fragWithHTML, unCamelCase } from '../utils/Utils';
|
||||
import type { PropertyMappingModelData } from './PropertyMapping';
|
||||
|
|
@ -14,12 +14,13 @@ import { FolderSuggest } from './suggesters/FolderSuggest';
|
|||
|
||||
// MARK: Settings
|
||||
export interface MediaDbPluginSettings {
|
||||
OMDbKey: string;
|
||||
TMDBKey: string;
|
||||
MobyGamesKey: string;
|
||||
GiantBombKey: string;
|
||||
ComicVineKey: string;
|
||||
BoardgameGeekKey: string;
|
||||
OMDbKeyId: string;
|
||||
TMDBKeyId: string;
|
||||
MobyGamesKeyId: string;
|
||||
GiantBombKeyId: string;
|
||||
ComicVineKeyId: string;
|
||||
BoardgameGeekKeyId: string;
|
||||
|
||||
sfwFilter: boolean;
|
||||
templates: boolean;
|
||||
customDateFormat: string;
|
||||
|
|
@ -267,12 +268,13 @@ class MediaTypeMappedSettings {
|
|||
|
||||
// MARK: Defaults
|
||||
const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||
OMDbKey: '',
|
||||
TMDBKey: '',
|
||||
MobyGamesKey: '',
|
||||
GiantBombKey: '',
|
||||
ComicVineKey: '',
|
||||
BoardgameGeekKey: '',
|
||||
OMDbKeyId: '',
|
||||
TMDBKeyId: '',
|
||||
MobyGamesKeyId: '',
|
||||
GiantBombKeyId: '',
|
||||
ComicVineKeyId: '',
|
||||
BoardgameGeekKeyId: '',
|
||||
|
||||
sfwFilter: true,
|
||||
templates: true,
|
||||
customDateFormat: 'L',
|
||||
|
|
@ -361,7 +363,7 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
|
|||
key,
|
||||
'',
|
||||
PropertyMappingOption.Default,
|
||||
lockedPropertyMappings.contains(key),
|
||||
lockedPropertyMappings.includes(key),
|
||||
false, // wikilink default
|
||||
),
|
||||
);
|
||||
|
|
@ -534,23 +536,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('OMDb API key')
|
||||
.setDesc('API key for "www.omdbapi.com".')
|
||||
// .addComponent((el) => {
|
||||
// let component = new SecretComponent(this.app, el);
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
// component.setValue(this.plugin.settings.OMDbKey).onChange(data => {
|
||||
// this.plugin.settings.OMDbKey = data;
|
||||
// void this.plugin.saveSettings();
|
||||
// });
|
||||
|
||||
// return component;
|
||||
// })
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.OMDbKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.OMDbKey = data;
|
||||
component.setValue(this.plugin.settings.OMDbKeyId).onChange(data => {
|
||||
this.plugin.settings.OMDbKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
|
|
@ -558,13 +552,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('TMDB API key')
|
||||
.setDesc('API key for "https://www.themoviedb.org".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.TMDBKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.TMDBKey = data;
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.TMDBKeyId).onChange(data => {
|
||||
this.plugin.settings.TMDBKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
|
|
@ -572,13 +568,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('Moby Games key')
|
||||
.setDesc('API key for "www.mobygames.com".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.MobyGamesKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.MobyGamesKey = data;
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.MobyGamesKeyId).onChange(data => {
|
||||
this.plugin.settings.MobyGamesKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
|
|
@ -586,13 +584,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('Giant Bomb Key')
|
||||
.setDesc('API key for "www.giantbomb.com".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.GiantBombKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.GiantBombKey = data;
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.GiantBombKeyId).onChange(data => {
|
||||
this.plugin.settings.GiantBombKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
|
|
@ -600,13 +600,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('Comic Vine Key')
|
||||
.setDesc('API key for "www.comicvine.gamespot.com".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.ComicVineKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.ComicVineKey = data;
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.ComicVineKeyId).onChange(data => {
|
||||
this.plugin.settings.ComicVineKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
apiKeyGroup.addSetting(
|
||||
|
|
@ -614,13 +616,15 @@ export class MediaDbSettingTab extends PluginSettingTab {
|
|||
void setting
|
||||
.setName('Boardgame Geek Key')
|
||||
.setDesc('API key for "www.boardgamegeek.com".')
|
||||
.addText(cb => {
|
||||
cb.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.BoardgameGeekKey)
|
||||
.onChange(data => {
|
||||
this.plugin.settings.BoardgameGeekKey = data;
|
||||
.addComponent(el => {
|
||||
const component = new SecretComponent(this.app, el);
|
||||
|
||||
component.setValue(this.plugin.settings.BoardgameGeekKeyId).onChange(data => {
|
||||
this.plugin.settings.BoardgameGeekKeyId = data;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
return component;
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export class FileSuggest extends AbstractInputSuggest<TFile> {
|
|||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFile)
|
||||
.filter(file => file.path.toLowerCase().contains(lowerCaseInputStr));
|
||||
.filter(file => file.path.toLowerCase().includes(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
renderSuggestion(value: TFile, el: HTMLElement): void {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export class FolderSuggest extends AbstractInputSuggest<TFolder> {
|
|||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(file => file instanceof TFolder)
|
||||
.filter(file => file.path.toLowerCase().contains(lowerCaseInputStr));
|
||||
.filter(file => file.path.toLowerCase().includes(lowerCaseInputStr));
|
||||
}
|
||||
|
||||
renderSuggestion(value: TFolder, el: HTMLElement): void {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { TFolder } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
import type MediaDbPlugin from 'src/main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from 'src/models/MediaTypeModel';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from '../modals/MediaDbBulkImportModal';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
import { ModalResultCode } from './ModalHelper';
|
||||
import { dateTimeToString, markdownTable } from './Utils';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { moment } from 'obsidian';
|
||||
import type momentType from 'moment';
|
||||
import type { Moment } from 'moment';
|
||||
import { moment as obsidianMoment } from 'obsidian';
|
||||
|
||||
const moment: typeof momentType = obsidianMoment as unknown as typeof momentType;
|
||||
|
||||
export class DateFormatter {
|
||||
private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
|
||||
|
||||
toFormat: string;
|
||||
locale: string;
|
||||
|
||||
|
|
@ -38,18 +44,10 @@ export class DateFormatter {
|
|||
return null;
|
||||
}
|
||||
|
||||
let date: moment.Moment;
|
||||
let date: Moment;
|
||||
|
||||
if (!dateFormat) {
|
||||
// reading date formats other then C2822 or ISO with moment is deprecated
|
||||
// see https://momentjs.com/docs/#/parsing/string/
|
||||
if (this.hasMomentFormat(dateString)) {
|
||||
// expect C2822 or ISO format
|
||||
date = moment(dateString);
|
||||
} else {
|
||||
// try to read date string with native Date
|
||||
date = moment(new Date(dateString));
|
||||
}
|
||||
date = this.parseWithoutFormat(dateString);
|
||||
} else {
|
||||
date = moment(dateString, dateFormat, locale);
|
||||
}
|
||||
|
|
@ -58,8 +56,20 @@ export class DateFormatter {
|
|||
return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null;
|
||||
}
|
||||
|
||||
private parseWithoutFormat(dateString: string): Moment {
|
||||
// reading date formats other than C2822 or ISO with moment is deprecated
|
||||
// see https://momentjs.com/docs/#/parsing/string/
|
||||
if (this.hasMomentFormat(dateString)) {
|
||||
// expect C2822 or ISO format
|
||||
return moment(dateString);
|
||||
}
|
||||
|
||||
// fall back to native Date parsing for unknown formats
|
||||
return moment(new Date(dateString));
|
||||
}
|
||||
|
||||
private hasMomentFormat(dateString: string): boolean {
|
||||
const date = moment(dateString, true); // strict mode
|
||||
const date = moment(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode
|
||||
return date.isValid();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal';
|
||||
import type MediaDbPlugin from '../main';
|
||||
import { MediaDbAdvancedSearchModal } from '../modals/MediaDbAdvancedSearchModal';
|
||||
import { MediaDbIdSearchModal } from '../modals/MediaDbIdSearchModal';
|
||||
import { MediaDbPreviewModal } from '../modals/MediaDbPreviewModal';
|
||||
import { MediaDbSearchModal } from '../modals/MediaDbSearchModal';
|
||||
import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal';
|
||||
import type { MediaTypeModel } from '../models/MediaTypeModel';
|
||||
|
|
|
|||
|
|
@ -223,10 +223,28 @@ export function unCamelCase(str: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
interface TemplaterPlugin {
|
||||
settings?: {
|
||||
trigger_on_file_creation?: boolean;
|
||||
};
|
||||
templater?: {
|
||||
overwrite_file_commands: (file: TFile) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
function getTemplaterPlugin(app: App): TemplaterPlugin | null {
|
||||
const pluginContainer = (app as App & { plugins?: { plugins?: Record<string, unknown> } }).plugins;
|
||||
const candidate = pluginContainer?.plugins?.['templater-obsidian'];
|
||||
|
||||
if (!candidate || typeof candidate !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate as TemplaterPlugin;
|
||||
}
|
||||
|
||||
export function hasTemplaterPlugin(app: App): boolean {
|
||||
const templater = (app as any).plugins.plugins['templater-obsidian'];
|
||||
const templater = getTemplaterPlugin(app);
|
||||
|
||||
return !!templater;
|
||||
}
|
||||
|
|
@ -234,17 +252,14 @@ export function hasTemplaterPlugin(app: App): boolean {
|
|||
// Copied from https://github.com/anpigon/obsidian-book-search-plugin
|
||||
// Licensed under the MIT license. Copyright (c) 2020 Jake Runzer
|
||||
export async function useTemplaterPluginInFile(app: App, file: TFile): Promise<void> {
|
||||
const templater = (app as any).plugins.plugins['templater-obsidian'];
|
||||
if (templater && !templater?.settings.trigger_on_file_creation) {
|
||||
const templater = getTemplaterPlugin(app);
|
||||
if (templater && !templater.settings?.trigger_on_file_creation && templater.templater) {
|
||||
await templater.templater.overwrite_file_commands(file);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
export type ModelToData<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
[K in keyof T as T[K] extends Function ? never : K]?: T[K] | null;
|
||||
[K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]?: T[K] | null;
|
||||
};
|
||||
|
||||
// Checks if a given URL points to an existing image (status 200), or returns false for 404/other errors.
|
||||
|
|
@ -272,8 +287,8 @@ export function isTruthy<T>(value: T): value is Exclude<T, false | 0 | '' | null
|
|||
*/
|
||||
export async function obsidianFetch(input: Request): Promise<Response> {
|
||||
const obs_headers: Record<string, string> = {};
|
||||
input.headers.forEach((header, value) => {
|
||||
obs_headers[header] = value;
|
||||
input.headers.forEach((value, key) => {
|
||||
obs_headers[key] = value;
|
||||
});
|
||||
|
||||
const res = await requestUrl({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
|
|
@ -9,7 +8,7 @@
|
|||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitReturns": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue