migrate to secret storage and other improvements

This commit is contained in:
Moritz Jung 2026-04-06 17:05:45 +02:00
parent faeca3459c
commit 7d6722c38e
28 changed files with 415 additions and 292 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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"
}
}

View file

@ -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);

View file

@ -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}`,
},
});

View file

@ -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,
});

View file

@ -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',
},
},

View file

@ -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';

View file

@ -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';

View file

@ -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,
});

View file

@ -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',
});

View file

@ -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';

View file

@ -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!),

View file

@ -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;
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<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 ?? ''),

View file

@ -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!),

View file

@ -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<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> {

View file

@ -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;

View file

@ -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';

View file

@ -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;
}

View file

@ -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) {
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<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
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];
}
}

View file

@ -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" />

View file

@ -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;
}),
);

View file

@ -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 {

View file

@ -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 {

View file

@ -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';

View file

@ -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();
}
}

View file

@ -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';

View file

@ -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({

View file

@ -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,