diff --git a/bun.lockb b/bun.lockb index 419cad8..9f23f83 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 92d08d1..815c67a 100644 --- a/package.json +++ b/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" } } diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 741c50e..00dca1a 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -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); diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 07c2098..bddddb7 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -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 { 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 { 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}`, }, }); diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index 53d9820..da7e7ad 100644 --- a/src/api/apis/ComicVineAPI.ts +++ b/src/api/apis/ComicVineAPI.ts @@ -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 { 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 { 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, }); diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 76e451b..6d6d1ea 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -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 { 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 { 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', }, }, diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index 5ac2cf9..5d27a61 100644 --- a/src/api/apis/MALAPI.ts +++ b/src/api/apis/MALAPI.ts @@ -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'; diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts index 83d4732..2d39a4f 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/src/api/apis/MALAPIManga.ts @@ -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'; diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index a88eec9..a8972b3 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -27,12 +27,13 @@ export class MobyGamesAPI extends APIModel { async searchByTitle(title: string): Promise { 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 { 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, }); diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 51b4f17..3862080 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -76,13 +76,14 @@ export class OMDbAPI extends APIModel { async searchByTitle(title: string): Promise { 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 { 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', }); diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts index 645d15b..f42f791 100644 --- a/src/api/apis/OpenLibraryAPI.ts +++ b/src/api/apis/OpenLibraryAPI.ts @@ -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'; diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 936ab57..4f4e174 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -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; @@ -27,15 +55,16 @@ export class TMDBMovieAPI extends APIModel { async searchByTitle(title: string): Promise { 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({ 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 { 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({ 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!), diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 426f6db..d4f9da6 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -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; @@ -27,15 +59,16 @@ export class TMDBSeasonAPI extends APIModel { async searchByTitle(title: string): Promise { 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({ 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; + 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 ${key}`, + }, + params: { + path: { series_id: result.id }, + }, + fetch: fetch, + }); - // Fetch series details to get the total number of seasons - let totalSeasons = 0; - try { - const detailsResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, - }, - params: { - path: { series_id: result.id ?? 0 }, - }, - 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 detail errors and use 0 as fallback. } } - } catch { - // Ignore errors and assume 0 seasons - } - 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 { - 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({ 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 { 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 "/season/".`); } - 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({ 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 ?? ''), diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 1c8a21f..cb6ce28 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -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; @@ -28,14 +47,15 @@ export class TMDBSeriesAPI extends APIModel { async searchByTitle(title: string): Promise { 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({ 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 { 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({ 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!), diff --git a/src/main.ts b/src/main.ts index df97525..56d415b 100644 --- a/src/main.ts +++ b/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,28 +373,22 @@ export default class MediaDbPlugin extends Plugin { return; } - let selectResults: MediaTypeModel[]; - const proceed: boolean = false; - - while (!proceed) { - selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; - if (!selectResults || selectResults.length < 1) { - return; - } - - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - break; + const selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + })) ?? []; + if (selectResults.length < 1) { + return; } - await this.createMediaDbNotes(selectResults!); + const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { + return previewModalData.confirmed; + }); + if (!confirmed) { + return; + } + + await this.createMediaDbNotes(selectResults); } async createEntryWithIdSearchModal(): Promise { @@ -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 { diff --git a/src/modals/MediaDbBulkImportModal.ts b/src/modals/MediaDbBulkImportModal.ts index 1aee8ee..0763d32 100644 --- a/src/modals/MediaDbBulkImportModal.ts +++ b/src/modals/MediaDbBulkImportModal.ts @@ -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; diff --git a/src/modals/MediaDbPreviewModal.ts b/src/modals/MediaDbPreviewModal.ts index 931556c..1efd66a 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/src/modals/MediaDbPreviewModal.ts @@ -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'; diff --git a/src/modals/MediaDbSearchModal.ts b/src/modals/MediaDbSearchModal.ts index 986cb8a..800eace 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/src/modals/MediaDbSearchModal.ts @@ -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; } diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index bf08d32..b34f245 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -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 = {}; for (const [key, value] of Object.entries(obj)) { - for (const propertyMapping of propertyMappings) { - if (propertyMapping.property === key) { - 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)); - } - } - 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; + 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)) { + finalValue = (value as unknown[]).map((v: unknown) => (typeof v === 'string' ? `[[${v}]]` : v)); } } + + if (propertyMapping.mapping === PropertyMappingOption.Map) { + newObj[propertyMapping.newProperty] = finalValue; + } else if (propertyMapping.mapping === PropertyMappingOption.Default) { + newObj[key] = finalValue; + } } 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 = {}; + const originalObj: Record = { ...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 - originalObj[key] = value; - - continue objLoop; - } + for (const [key, value] of Object.entries(obj)) { + const normalProperty = propertyMappingByOriginal.get(key); + if (normalProperty) { + originalObj[key] = value; + 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]; } } diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index d9a9017..c31bdbf 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -96,7 +96,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode —} + fallback={N/A} >
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 4c24635..1b29fb3 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -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(); - // }); + component.setValue(this.plugin.settings.OMDbKeyId).onChange(data => { + this.plugin.settings.OMDbKeyId = 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; - 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; - void this.plugin.saveSettings(); - }); + .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; - void this.plugin.saveSettings(); - }); + .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; - void this.plugin.saveSettings(); - }); + .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; - void this.plugin.saveSettings(); - }); + .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; - void this.plugin.saveSettings(); - }); + .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; }), ); diff --git a/src/settings/suggesters/FileSuggest.ts b/src/settings/suggesters/FileSuggest.ts index 6bbdb96..6e91100 100644 --- a/src/settings/suggesters/FileSuggest.ts +++ b/src/settings/suggesters/FileSuggest.ts @@ -8,7 +8,7 @@ export class FileSuggest extends AbstractInputSuggest { 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 { diff --git a/src/settings/suggesters/FolderSuggest.ts b/src/settings/suggesters/FolderSuggest.ts index eaee715..2ce3574 100644 --- a/src/settings/suggesters/FolderSuggest.ts +++ b/src/settings/suggesters/FolderSuggest.ts @@ -8,7 +8,7 @@ export class FolderSuggest extends AbstractInputSuggest { 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 { diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts index dcbaabb..da4b1a4 100644 --- a/src/utils/BulkImportHelper.ts +++ b/src/utils/BulkImportHelper.ts @@ -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'; diff --git a/src/utils/DateFormatter.ts b/src/utils/DateFormatter.ts index 0088af0..31169e5 100644 --- a/src/utils/DateFormatter.ts +++ b/src/utils/DateFormatter.ts @@ -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(); } } diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts index 7efba29..a8d1503 100644 --- a/src/utils/ModalHelper.ts +++ b/src/utils/ModalHelper.ts @@ -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'; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 9b25b34..9e4f286 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -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; + }; +} + +function getTemplaterPlugin(app: App): TemplaterPlugin | null { + const pluginContainer = (app as App & { plugins?: { plugins?: Record } }).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 { - 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 = { - // 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(value: T): value is Exclude { const obs_headers: Record = {}; - input.headers.forEach((header, value) => { - obs_headers[header] = value; + input.headers.forEach((value, key) => { + obs_headers[key] = value; }); const res = await requestUrl({ diff --git a/tsconfig.json b/tsconfig.json index c6eb672..49b6fcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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,