From 3988bf4608d739a78bec3954ad43b7407a2aa787 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 16 Feb 2026 04:14:04 +0100 Subject: [PATCH 01/35] Add support for images in media search - Auto fetches images with a small delay for rate limits - If the link to the image fails to load then it uses a placeholder emoticon - Also fixed the bug where steamapi doesn't show the year during the selection screen (only afterwards) --- src/modals/MediaDbSearchResultModal.ts | 89 +++++++++++++++++++++++++- src/styles.css | 40 ++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 786df40a..1c1a90e7 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -41,9 +41,92 @@ export class MediaDbSearchResultModal extends SelectModal { // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); - el.createEl('small', { text: `${item.getSummary()}\n` }); - el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); + el.addClass('media-db-plugin-select-element-flex'); + el.style.display = 'flex'; + el.style.flexDirection = 'row'; + el.style.gap = '8px'; + el.style.alignItems = 'flex-start'; + + const thumb = el.createDiv({ cls: 'media-db-plugin-select-thumb' }); + + let imgEl: HTMLImageElement | undefined; + const setImage = (url: string) => { + if (!imgEl) { + imgEl = document.createElement('img'); + imgEl.loading = 'lazy'; + imgEl.alt = item.title; + thumb.empty(); + thumb.appendChild(imgEl); + thumb.style.width = '48px'; + thumb.style.height = '72px'; + thumb.style.flex = '0 0 48px'; + thumb.style.overflow = 'hidden'; + imgEl.style.width = '100%'; + imgEl.style.height = '100%'; + imgEl.style.objectFit = 'cover'; + // Show photograph emoticon if the link to the image fails to load + imgEl.onerror = () => { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + placeholderSpan.style.display = 'flex'; + placeholderSpan.style.alignItems = 'center'; + placeholderSpan.style.justifyContent = 'center'; + placeholderSpan.style.width = '100%'; + placeholderSpan.style.height = '100%'; + }; + } + imgEl.src = url; + }; + + // Create content early so updateSummary can reference its elements + const content = el.createDiv({ cls: 'media-db-plugin-select-content' }); + const titleEl = content.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title' }); + const summaryEl = content.createEl('small', { text: `${item.getSummary()}\n` }); + content.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); + + // Helper to update both title and summary when year is fetched + const updateSummary = () => { + titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); + summaryEl.textContent = `${item.getSummary()}\n`; + }; + + if (item.image && item.image !== 'NSFW') { + if (String(item.image).includes('null')) { + console.debug('MDB | image URL invalid (contains null), skipping', item.image); + } else { + setImage(item.image); + } + } else if (item.image === 'NSFW') { + thumb.createEl('span', { text: 'NSFW' }); + } else { + thumb.createEl('span', { text: '' }); + // Auto-fetch detailed info with staggered delays to avoid rate limits + fetch detailed info if no image (most API's except for MusicBrainz) OR no year (like SteamAPI) + const needsFetch = !item.image || !item.year; + if (needsFetch) { + const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * 200; + console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms'); + setTimeout(async () => { + if (item.image && item.year) return; + console.debug('MDB | auto-fetching detail for', item.dataSource, item.id); + try { + console.debug('MDB | fetching detailed info for', item.dataSource, item.id); + const detailed = await this.plugin.apiManager.queryDetailedInfo(item); + console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year); + if (detailed?.image && !item.image) { + item.image = detailed.image; + setImage(detailed.image); + } + if (!item.year && detailed?.year) { + item.year = detailed.year; + updateSummary(); + } + } catch (e) { + console.warn('MDB | Failed to fetch detail', e); + } + }, delayMs); + } + } } // Perform action on the selected suggestion. diff --git a/src/styles.css b/src/styles.css index b204d1d6..9fa5392c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -41,6 +41,46 @@ small.media-db-plugin-list-text { font-size: 16px; } +.media-db-plugin-select-element-flex { + display: flex; + gap: 10px; + align-items: flex-start; +} + +.media-db-plugin-select-thumb { + width: 56px; + height: 84px; + flex: 0 0 56px; + background: var(--background-modifier-hover); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + overflow: hidden; +} + +.media-db-plugin-select-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.media-db-plugin-select-content { + flex: 1; + min-width: 0; +} + +.media-db-plugin-select-title { + font-weight: 600; +} + +.media-db-plugin-select-thumb span { + color: var(--text-muted); + font-size: 12px; + text-align: center; +} + .media-db-plugin-select-element-selected { border-left: 5px solid var(--interactive-accent) !important; background: var(--background-secondary-alt); From e4a8e2aea26e38005506b907d2b22ebdfbd2df7c Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:56:20 +0100 Subject: [PATCH 02/35] Added images to season search selection --- src/api/apis/TMDBSeasonAPI.ts | 2 ++ src/modals/MediaDbSearchResultModal.ts | 17 ++++++++++- src/modals/MediaDbSeasonSelectModal.ts | 42 ++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 426f6db5..557eb4e6 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -97,6 +97,7 @@ export class TMDBSeasonAPI extends APIModel { id: result.id?.toString() ?? '', seasonTitle: result.name ?? result.original_name ?? '', seasonNumber: totalSeasons, + image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', }), ); } @@ -148,6 +149,7 @@ export class TMDBSeasonAPI extends APIModel { id: `${tvId}/season/${seasonNumber}`, seasonTitle: season.name ?? titleText, seasonNumber: seasonNumber, + image: season.poster_path ?? '', }), ); } diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 1c1a90e7..5796cf1a 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -94,13 +94,28 @@ export class MediaDbSearchResultModal extends SelectModal { if (item.image && item.image !== 'NSFW') { if (String(item.image).includes('null')) { console.debug('MDB | image URL invalid (contains null), skipping', item.image); + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + placeholderSpan.style.display = 'flex'; + placeholderSpan.style.alignItems = 'center'; + placeholderSpan.style.justifyContent = 'center'; + placeholderSpan.style.width = '100%'; + placeholderSpan.style.height = '100%'; } else { setImage(item.image); } } else if (item.image === 'NSFW') { thumb.createEl('span', { text: 'NSFW' }); } else { - thumb.createEl('span', { text: '' }); + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + placeholderSpan.style.display = 'flex'; + placeholderSpan.style.alignItems = 'center'; + placeholderSpan.style.justifyContent = 'center'; + placeholderSpan.style.width = '100%'; + placeholderSpan.style.height = '100%'; // Auto-fetch detailed info with staggered delays to avoid rate limits + fetch detailed info if no image (most API's except for MusicBrainz) OR no year (like SteamAPI) const needsFetch = !item.image || !item.year; if (needsFetch) { diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index 0066c31e..4c4b91da 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -24,9 +24,47 @@ export class MediaDbSeasonSelectModal extends SelectModal { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '\ud83d\udcf7' }); + placeholderSpan.style.fontSize = '24px'; + }; + thumb.appendChild(img); + } else { + thumb.createEl('span', { text: '\ud83d\udcf7' }).style.fontSize = '24px'; + } + + const content = el.createDiv(); + content.style.flex = '1'; + content.style.minWidth = '0'; + content.createEl('div', { text: `${season.name}` }); if (season.air_date) { - el.createEl('small', { text: `Air date: ${season.air_date}` }); + content.createEl('small', { text: `Air date: ${season.air_date}` }); } } From 47a78d556f3d50ee991c447b94ce9240f57c3d9a Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:18:50 +0100 Subject: [PATCH 03/35] Ran prettier --- src/modals/MediaDbSeasonSelectModal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index 4c4b91da..f5d4586b 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -41,9 +41,7 @@ export class MediaDbSeasonSelectModal extends SelectModal Date: Mon, 16 Feb 2026 14:55:38 +0100 Subject: [PATCH 04/35] Normalize css between files --- src/modals/MediaDbSearchResultModal.ts | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 5796cf1a..f1efa050 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -43,13 +43,22 @@ export class MediaDbSearchResultModal extends SelectModal { renderElement(item: MediaTypeModel, el: HTMLElement): void { el.addClass('media-db-plugin-select-element-flex'); el.style.display = 'flex'; - el.style.flexDirection = 'row'; el.style.gap = '8px'; el.style.alignItems = 'flex-start'; const thumb = el.createDiv({ cls: 'media-db-plugin-select-thumb' }); + thumb.style.width = '48px'; + thumb.style.height = '72px'; + thumb.style.flex = '0 0 48px'; + thumb.style.overflow = 'hidden'; + thumb.style.background = 'var(--background-modifier-hover)'; + thumb.style.borderRadius = '4px'; + thumb.style.display = 'flex'; + thumb.style.alignItems = 'center'; + thumb.style.justifyContent = 'center'; let imgEl: HTMLImageElement | undefined; + const setImage = (url: string) => { if (!imgEl) { imgEl = document.createElement('img'); @@ -57,30 +66,27 @@ export class MediaDbSearchResultModal extends SelectModal { imgEl.alt = item.title; thumb.empty(); thumb.appendChild(imgEl); - thumb.style.width = '48px'; - thumb.style.height = '72px'; - thumb.style.flex = '0 0 48px'; - thumb.style.overflow = 'hidden'; + imgEl.style.width = '100%'; imgEl.style.height = '100%'; imgEl.style.objectFit = 'cover'; + // Show photograph emoticon if the link to the image fails to load imgEl.onerror = () => { thumb.empty(); const placeholderSpan = thumb.createEl('span', { text: '📷' }); placeholderSpan.style.fontSize = '24px'; - placeholderSpan.style.display = 'flex'; - placeholderSpan.style.alignItems = 'center'; - placeholderSpan.style.justifyContent = 'center'; - placeholderSpan.style.width = '100%'; - placeholderSpan.style.height = '100%'; }; } + imgEl.src = url; }; // Create content early so updateSummary can reference its elements const content = el.createDiv({ cls: 'media-db-plugin-select-content' }); + content.style.flex = '1'; + content.style.minWidth = '0'; + const titleEl = content.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title' }); const summaryEl = content.createEl('small', { text: `${item.getSummary()}\n` }); content.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); @@ -97,11 +103,6 @@ export class MediaDbSearchResultModal extends SelectModal { thumb.empty(); const placeholderSpan = thumb.createEl('span', { text: '📷' }); placeholderSpan.style.fontSize = '24px'; - placeholderSpan.style.display = 'flex'; - placeholderSpan.style.alignItems = 'center'; - placeholderSpan.style.justifyContent = 'center'; - placeholderSpan.style.width = '100%'; - placeholderSpan.style.height = '100%'; } else { setImage(item.image); } @@ -111,16 +112,13 @@ export class MediaDbSearchResultModal extends SelectModal { thumb.empty(); const placeholderSpan = thumb.createEl('span', { text: '📷' }); placeholderSpan.style.fontSize = '24px'; - placeholderSpan.style.display = 'flex'; - placeholderSpan.style.alignItems = 'center'; - placeholderSpan.style.justifyContent = 'center'; - placeholderSpan.style.width = '100%'; - placeholderSpan.style.height = '100%'; + // Auto-fetch detailed info with staggered delays to avoid rate limits + fetch detailed info if no image (most API's except for MusicBrainz) OR no year (like SteamAPI) const needsFetch = !item.image || !item.year; if (needsFetch) { const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * 200; console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms'); + setTimeout(async () => { if (item.image && item.year) return; console.debug('MDB | auto-fetching detail for', item.dataSource, item.id); @@ -128,10 +126,12 @@ export class MediaDbSearchResultModal extends SelectModal { console.debug('MDB | fetching detailed info for', item.dataSource, item.id); const detailed = await this.plugin.apiManager.queryDetailedInfo(item); console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year); + if (detailed?.image && !item.image) { item.image = detailed.image; setImage(detailed.image); } + if (!item.year && detailed?.year) { item.year = detailed.year; updateSummary(); From f8939d2f39b290a21e9ab864f0dba5b68e2ec054 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:02:49 +0100 Subject: [PATCH 05/35] Update styles.css --- src/styles.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles.css b/src/styles.css index 9fa5392c..b532da6a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -48,9 +48,9 @@ small.media-db-plugin-list-text { } .media-db-plugin-select-thumb { - width: 56px; - height: 84px; - flex: 0 0 56px; + width: 48px; + height: 72px; + flex: 0 0 48px; background: var(--background-modifier-hover); display: flex; align-items: center; From c78f7f902b5ed0041aef03e30234c794785ad30f Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:03:47 +0100 Subject: [PATCH 06/35] Different rate limit for MALAPI and MALAPIManga --- src/modals/MediaDbSearchResultModal.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index f1efa050..71806a80 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -39,6 +39,12 @@ export class MediaDbSearchResultModal extends SelectModal { this.skipCallback = skipCallback; } + // Different rate limit delay based on API source, MAL APIs = max 3 per second so 400ms between requests to be safe + private getDelayForApi(dataSource: string): number { + const isMalApi = dataSource === 'MALAPI' || dataSource === 'MALAPIManga'; + return isMalApi ? 400 : 200; + } + // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { el.addClass('media-db-plugin-select-element-flex'); @@ -116,8 +122,9 @@ export class MediaDbSearchResultModal extends SelectModal { // Auto-fetch detailed info with staggered delays to avoid rate limits + fetch detailed info if no image (most API's except for MusicBrainz) OR no year (like SteamAPI) const needsFetch = !item.image || !item.year; if (needsFetch) { - const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * 200; - console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms'); + const apiDelay = this.getDelayForApi(item.dataSource); + const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * apiDelay; + console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`); setTimeout(async () => { if (item.image && item.year) return; From 33a5c9cfc8f126df75204b90184cb3bbbed3490a Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:13:36 +0200 Subject: [PATCH 07/35] Refactor thumbnails to js instead of hardcoded css - Removed the css from styles.css, MediaDbSearchResultModal and MediaDbSeasonSelectModal - Created "MediaItemComponent" to replace the hardcoded css in a centralized manner --- src/modals/MediaDbSearchResultModal.ts | 176 +++++++++++-------------- src/modals/MediaDbSeasonSelectModal.ts | 55 +++----- src/modals/MediaItemComponent.ts | 112 ++++++++++++++++ src/styles.css | 36 ----- 4 files changed, 205 insertions(+), 174 deletions(-) create mode 100644 src/modals/MediaItemComponent.ts diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 71806a80..3c01c8f3 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -2,6 +2,7 @@ import type MediaDbPlugin from '../main'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; +import { MediaItemComponent } from './MediaItemComponent'; import { SelectModal } from './SelectModal'; export class MediaDbSearchResultModal extends SelectModal { @@ -47,108 +48,87 @@ export class MediaDbSearchResultModal extends SelectModal { // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.addClass('media-db-plugin-select-element-flex'); - el.style.display = 'flex'; - el.style.gap = '8px'; - el.style.alignItems = 'flex-start'; - - const thumb = el.createDiv({ cls: 'media-db-plugin-select-thumb' }); - thumb.style.width = '48px'; - thumb.style.height = '72px'; - thumb.style.flex = '0 0 48px'; - thumb.style.overflow = 'hidden'; - thumb.style.background = 'var(--background-modifier-hover)'; - thumb.style.borderRadius = '4px'; - thumb.style.display = 'flex'; - thumb.style.alignItems = 'center'; - thumb.style.justifyContent = 'center'; - - let imgEl: HTMLImageElement | undefined; - - const setImage = (url: string) => { - if (!imgEl) { - imgEl = document.createElement('img'); - imgEl.loading = 'lazy'; - imgEl.alt = item.title; - thumb.empty(); - thumb.appendChild(imgEl); - - imgEl.style.width = '100%'; - imgEl.style.height = '100%'; - imgEl.style.objectFit = 'cover'; - - // Show photograph emoticon if the link to the image fails to load - imgEl.onerror = () => { - thumb.empty(); - const placeholderSpan = thumb.createEl('span', { text: '📷' }); - placeholderSpan.style.fontSize = '24px'; - }; - } - - imgEl.src = url; - }; - - // Create content early so updateSummary can reference its elements - const content = el.createDiv({ cls: 'media-db-plugin-select-content' }); - content.style.flex = '1'; - content.style.minWidth = '0'; - - const titleEl = content.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title' }); - const summaryEl = content.createEl('small', { text: `${item.getSummary()}\n` }); - content.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); - - // Helper to update both title and summary when year is fetched - const updateSummary = () => { - titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); - summaryEl.textContent = `${item.getSummary()}\n`; - }; + // Create the media item component + const mediaComponent = new MediaItemComponent(el, { + imageUrl: this.getImageUrl(item), + imageAlt: item.title, + onImageError: () => { + console.debug('MDB | Image failed to load for', item.id); + }, + onImageLoad: () => { + console.debug('MDB | Image loaded for', item.id); + }, + renderContent: (contentEl) => { + const titleEl = contentEl.createEl('div', { + text: this.plugin.mediaTypeManager.getFileName(item), + cls: 'media-db-plugin-select-title', + }); + const summaryEl = contentEl.createEl('small', { text: `${item.getSummary()}\n` }); + contentEl.createEl('small', { + text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`, + }); + + // Store references for later updates + (item as any).__titleEl = titleEl; + (item as any).__summaryEl = summaryEl; + }, + }); + + // Auto-fetch detailed info if needed + this.autoFetchDetails(item, mediaComponent); + } + private getImageUrl(item: MediaTypeModel): string | undefined { if (item.image && item.image !== 'NSFW') { - if (String(item.image).includes('null')) { - console.debug('MDB | image URL invalid (contains null), skipping', item.image); - thumb.empty(); - const placeholderSpan = thumb.createEl('span', { text: '📷' }); - placeholderSpan.style.fontSize = '24px'; - } else { - setImage(item.image); - } - } else if (item.image === 'NSFW') { - thumb.createEl('span', { text: 'NSFW' }); - } else { - thumb.empty(); - const placeholderSpan = thumb.createEl('span', { text: '📷' }); - placeholderSpan.style.fontSize = '24px'; - - // Auto-fetch detailed info with staggered delays to avoid rate limits + fetch detailed info if no image (most API's except for MusicBrainz) OR no year (like SteamAPI) - const needsFetch = !item.image || !item.year; - if (needsFetch) { - const apiDelay = this.getDelayForApi(item.dataSource); - const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * apiDelay; - console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`); - - setTimeout(async () => { - if (item.image && item.year) return; - console.debug('MDB | auto-fetching detail for', item.dataSource, item.id); - try { - console.debug('MDB | fetching detailed info for', item.dataSource, item.id); - const detailed = await this.plugin.apiManager.queryDetailedInfo(item); - console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year); - - if (detailed?.image && !item.image) { - item.image = detailed.image; - setImage(detailed.image); - } - - if (!item.year && detailed?.year) { - item.year = detailed.year; - updateSummary(); - } - } catch (e) { - console.warn('MDB | Failed to fetch detail', e); - } - }, delayMs); + if (!String(item.image).includes('null')) { + return item.image; } } + return item.image; + } + + private autoFetchDetails(item: MediaTypeModel, mediaComponent: MediaItemComponent): void { + const needsFetch = !item.image || !item.year; + if (!needsFetch) return; + + const apiDelay = this.getDelayForApi(item.dataSource); + const element = document.getElementById(`media-db-plugin-select-element-${this.selectModalElements.length}`); + const delayMs = element ? (parseInt(element.id.split('-').pop() ?? '0') ?? 0) * apiDelay : 0; + + console.debug( + 'MDB | will auto-fetch detail for', + item.dataSource, + item.id, + 'in', + delayMs, + 'ms', + `(${apiDelay}ms per request)` + ); + + setTimeout(async () => { + if (item.image && item.year) return; + console.debug('MDB | auto-fetching detail for', item.dataSource, item.id); + try { + console.debug('MDB | fetching detailed info for', item.dataSource, item.id); + const detailed = await this.plugin.apiManager.queryDetailedInfo(item); + console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year); + + if (detailed?.image && !item.image) { + item.image = detailed.image; + mediaComponent.updateImage(item.image); + } + + if (!item.year && detailed?.year) { + item.year = detailed.year; + const titleEl = (item as any).__titleEl; + const summaryEl = (item as any).__summaryEl; + if (titleEl) titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); + if (summaryEl) summaryEl.textContent = `${item.getSummary()}\n`; + } + } catch (e) { + console.warn('MDB | Failed to fetch detail', e); + } + }, delayMs); } // Perform action on the selected suggestion. diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index f5d4586b..ee4d1d5f 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -1,4 +1,5 @@ import type MediaDbPlugin from '../main'; +import { MediaItemComponent } from './MediaItemComponent'; import { SelectModal } from './SelectModal'; export interface SeasonSelectModalElement { @@ -24,46 +25,20 @@ export class MediaDbSeasonSelectModal extends SelectModal { - thumb.empty(); - const placeholderSpan = thumb.createEl('span', { text: '\ud83d\udcf7' }); - placeholderSpan.style.fontSize = '24px'; - }; - thumb.appendChild(img); - } else { - thumb.createEl('span', { text: '\ud83d\udcf7' }).style.fontSize = '24px'; - } - - const content = el.createDiv(); - content.style.flex = '1'; - content.style.minWidth = '0'; - content.createEl('div', { text: `${season.name}` }); - if (season.air_date) { - content.createEl('small', { text: `Air date: ${season.air_date}` }); - } + new MediaItemComponent(el, { + imageUrl: season.poster_path + ? season.poster_path.startsWith('http') + ? season.poster_path + : `https://image.tmdb.org/t/p/w780${season.poster_path}` + : undefined, + imageAlt: season.name, + renderContent: (contentEl) => { + contentEl.createEl('div', { text: `${season.name}` }); + if (season.air_date) { + contentEl.createEl('small', { text: `Air date: ${season.air_date}` }); + } + }, + }); } submit(): void { diff --git a/src/modals/MediaItemComponent.ts b/src/modals/MediaItemComponent.ts new file mode 100644 index 00000000..b2d668ee --- /dev/null +++ b/src/modals/MediaItemComponent.ts @@ -0,0 +1,112 @@ +export interface MediaItemComponentOptions { + imageUrl?: string; + imageAlt?: string; + onImageError?: () => void; + onImageLoad?: () => void; + renderContent: (contentEl: HTMLElement) => void; +} + +export class MediaItemComponent { + private container: HTMLElement; + private thumbEl: HTMLElement; + private contentEl: HTMLElement; + private imgEl: HTMLImageElement | undefined; + private options: MediaItemComponentOptions; + + constructor(container: HTMLElement, options: MediaItemComponentOptions) { + this.container = container; + this.options = options; + this.thumbEl = null as any; // Will be initialized in setup + this.contentEl = null as any; + + this.setup(); + } + + private setup(): void { + // Set container layout + this.container.addClass('media-item-component'); + this.container.style.display = 'flex'; + this.container.style.gap = '8px'; + this.container.style.alignItems = 'flex-start'; + + // Create thumbnail + this.thumbEl = this.container.createDiv({ cls: 'media-item-thumb' }); + this.thumbEl.style.width = '48px'; + this.thumbEl.style.height = '72px'; + this.thumbEl.style.flex = '0 0 48px'; + this.thumbEl.style.overflow = 'hidden'; + this.thumbEl.style.background = 'var(--background-modifier-hover)'; + this.thumbEl.style.borderRadius = '4px'; + this.thumbEl.style.display = 'flex'; + this.thumbEl.style.alignItems = 'center'; + this.thumbEl.style.justifyContent = 'center'; + + // Create content area + this.contentEl = this.container.createDiv({ cls: 'media-item-content' }); + this.contentEl.style.flex = '1'; + this.contentEl.style.minWidth = '0'; + + // Render custom content + this.options.renderContent(this.contentEl); + + // Setup image if provided + if (this.options.imageUrl) { + this.loadImage(this.options.imageUrl); + } else { + this.showPlaceholder(); + } + } + + private loadImage(url: string): void { + if (!this.imgEl) { + this.imgEl = document.createElement('img'); + this.imgEl.loading = 'lazy'; + this.imgEl.alt = this.options.imageAlt || 'Media item'; + this.imgEl.className = 'media-item-image'; + this.imgEl.style.width = '100%'; + this.imgEl.style.height = '100%'; + this.imgEl.style.objectFit = 'cover'; + this.imgEl.style.display = 'block'; + + this.imgEl.onerror = () => { + this.showPlaceholder(); + this.options.onImageError?.(); + }; + + this.imgEl.onload = () => { + this.options.onImageLoad?.(); + }; + + this.thumbEl.empty(); + this.thumbEl.appendChild(this.imgEl); + } + + this.imgEl.src = url; + } + + private showPlaceholder(): void { + this.thumbEl.empty(); + const span = this.thumbEl.createEl('span', { text: '📷' }); + span.style.fontSize = '24px'; + span.style.color = 'var(--text-muted)'; + } + + public updateImage(url: string | undefined): void { + if (url && url !== 'NSFW') { + if (!String(url).includes('null')) { + this.loadImage(url); + } else { + this.showPlaceholder(); + } + } else if (url === 'NSFW') { + this.thumbEl.empty(); + this.thumbEl.createEl('span', { text: 'NSFW' }); + } else { + this.showPlaceholder(); + } + } + + public getContentElement(): HTMLElement { + return this.contentEl; + } +} diff --git a/src/styles.css b/src/styles.css index b532da6a..afdfcc6f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -41,46 +41,10 @@ small.media-db-plugin-list-text { font-size: 16px; } -.media-db-plugin-select-element-flex { - display: flex; - gap: 10px; - align-items: flex-start; -} - -.media-db-plugin-select-thumb { - width: 48px; - height: 72px; - flex: 0 0 48px; - background: var(--background-modifier-hover); - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - overflow: hidden; -} - -.media-db-plugin-select-thumb img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.media-db-plugin-select-content { - flex: 1; - min-width: 0; -} - .media-db-plugin-select-title { font-weight: 600; } -.media-db-plugin-select-thumb span { - color: var(--text-muted); - font-size: 12px; - text-align: center; -} - .media-db-plugin-select-element-selected { border-left: 5px solid var(--interactive-accent) !important; background: var(--background-secondary-alt); From 2ba460d6cbe26dd94d688e4aa2f743418caea153 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:15:29 +0200 Subject: [PATCH 08/35] Ran prettier --- src/main.ts | 2 +- src/modals/MediaDbSearchResultModal.ts | 12 ++---------- src/modals/MediaDbSeasonSelectModal.ts | 8 ++------ src/settings/Settings.ts | 2 +- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/main.ts b/src/main.ts index 7f7019ed..520516f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -732,4 +732,4 @@ export default class MediaDbPlugin extends Plugin { await this.saveData(this.settings); } -} \ No newline at end of file +} diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 3c01c8f3..e258b95c 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -58,7 +58,7 @@ export class MediaDbSearchResultModal extends SelectModal { onImageLoad: () => { console.debug('MDB | Image loaded for', item.id); }, - renderContent: (contentEl) => { + renderContent: contentEl => { const titleEl = contentEl.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title', @@ -95,15 +95,7 @@ export class MediaDbSearchResultModal extends SelectModal { const element = document.getElementById(`media-db-plugin-select-element-${this.selectModalElements.length}`); const delayMs = element ? (parseInt(element.id.split('-').pop() ?? '0') ?? 0) * apiDelay : 0; - console.debug( - 'MDB | will auto-fetch detail for', - item.dataSource, - item.id, - 'in', - delayMs, - 'ms', - `(${apiDelay}ms per request)` - ); + console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`); setTimeout(async () => { if (item.image && item.year) return; diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index ee4d1d5f..be3a1ae5 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -26,13 +26,9 @@ export class MediaDbSeasonSelectModal extends SelectModal { + renderContent: contentEl => { contentEl.createEl('div', { text: `${season.name}` }); if (season.air_date) { contentEl.createEl('small', { text: `Air date: ${season.air_date}` }); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 9566bfc0..f838410a 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -829,4 +829,4 @@ export class MediaDbSettingTab extends PluginSettingTab { }); } } -} \ No newline at end of file +} From 0bc0c88df19a51d6f34c02a3da6bd06c4e50a794 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Sun, 7 Jun 2026 02:13:35 +0200 Subject: [PATCH 09/35] Revert "Merge branch 'master' into image2" This reverts commit 3fd393f54d1d563c29487bb80b3b1ee83829ea4c, reversing changes made to 2ba460d6cbe26dd94d688e4aa2f743418caea153. --- CHANGELOG.md | 26 - automation/build/esbuild.config.ts | 57 ++ automation/build/esbuild.dev.config.ts | 65 ++ automation/fetchSchemas.ts | 2 +- automation/release.ts | 6 +- automation/tsconfig.json | 5 +- automation/utils/versionUtils.ts | 2 +- bun.lock | 810 ------------------ bun.lockb | Bin 0 -> 211573 bytes manifest-beta.json | 2 +- package.json | 28 +- src/api/APIManager.ts | 4 +- src/api/apis/BoardGameGeekAPI.ts | 14 +- src/api/apis/ComicVineAPI.ts | 14 +- src/api/apis/GiantBombAPI.ts | 12 +- src/api/apis/IGDBAPI.ts | 100 +-- src/api/apis/MALAPI.ts | 2 +- src/api/apis/MALAPIManga.ts | 2 +- src/api/apis/MobyGamesAPI.ts | 10 +- src/api/apis/OMDbAPI.ts | 10 +- src/api/apis/OpenLibraryAPI.ts | 4 +- src/api/apis/RAWGAPI.ts | 86 +- src/api/apis/TMDBMovieAPI.ts | 57 +- src/api/apis/TMDBSeasonAPI.ts | 141 ++- src/api/apis/TMDBSeriesAPI.ts | 44 +- src/main.ts | 60 +- src/modals/MediaDbBulkImportModal.ts | 4 +- src/modals/MediaDbPreviewModal.ts | 4 +- src/modals/MediaDbSearchModal.ts | 2 +- src/settings/PropertyMapper.ts | 79 +- .../PropertyMappingModelComponent.tsx | 2 +- src/settings/Settings.ts | 188 ++-- src/settings/suggesters/FileSuggest.ts | 2 +- src/settings/suggesters/FolderSuggest.ts | 2 +- src/utils/BulkImportHelper.ts | 6 +- src/utils/DateFormatter.ts | 34 +- src/utils/ModalHelper.ts | 2 +- src/utils/Utils.ts | 35 +- tsconfig.json | 3 +- vite.config.ts | 4 +- 40 files changed, 500 insertions(+), 1430 deletions(-) create mode 100644 automation/build/esbuild.config.ts create mode 100644 automation/build/esbuild.dev.config.ts delete mode 100644 bun.lock create mode 100755 bun.lockb diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d32378..d9f4a1c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,5 @@ # Changelog -# Unreleased - -- Added support for the `VNDB` API [#165](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/165) (thanks Senyksia) -- Added support for comic books through the `Comic Vine` API [#176](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/176) (thanks ltctceplrm) -- Fixed a typo issue in Steam search [#180](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/180) (thanks ltctceplrm) -- Added API toggle improvements and image download options [#183](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/183) (thanks ltctceplrm) -- Added a confirmation step before overwriting existing notes [#184](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/184) (thanks ltctceplrm) -- Improved Open Library integration and added plot/description support [#190](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/190) [#192](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/192) [#237](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/237) [#238](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/238) (thanks ltctceplrm) -- Added country, box office and age rating fields for movies and series [#196](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/196) (thanks ltctceplrm) -- Added release date support for MusicBrainz releases [#198](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/198) (thanks ZackBoe) -- Limited MusicBrainz cover art images to 500px [#199](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/199) (thanks ZackBoe) -- Added tracks, language, track count, and total duration support for music releases [#202](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/202) [#233](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/233) (thanks ltctceplrm) -- Cleaned up generated file names to remove illegal characters [#206](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/206) (thanks Spydarlee) -- Added batch/folder import by ID [#207](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/207) (thanks Spydarlee) -- Fixed double URI encoding when querying the Giant Bomb API by title [#209](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/209) (thanks Spydarlee) -- Improved property mappings with table view, wiki links option, and bug fixes [#195](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/195) [#231](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/231) (thanks ltctceplrm) -- Added TMDB season selection modal and improved season metadata handling [#220](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/220) (thanks ltctceplrm) -- Added board game API authorization and improved related error handling [#223](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/223) [#227](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/227) (thanks ltctceplrm) -- Swapped TMDB API key usage to TMDB API token [#246](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/246) (thanks ZackBoe) -- Updated comic/manga tags to vary based on subtype [#247](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/247) (thanks ltctceplrm) -- Re-implemented `IGDB` and `RAWG` providers with improved type safety and conflict resolution [#239](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/239) (thanks m24ih) -- Added support for Japanese titles in MyAnimeList [#253](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/253) (thanks ltctceplrm) -- Migrated plugin secrets to Obsidian secret storage -- Migrated settings UI from Svelte to Solid -- Various internal refactors, dependency updates, and bug fixes - # 0.8.0 - Fixed bugs when API keys for certain APIs were missing [#161](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/161) (thanks ltctceplrm) diff --git a/automation/build/esbuild.config.ts b/automation/build/esbuild.config.ts new file mode 100644 index 00000000..20d35b8f --- /dev/null +++ b/automation/build/esbuild.config.ts @@ -0,0 +1,57 @@ +import builtins from 'builtin-modules'; +import esbuild from 'esbuild'; +import esbuildSvelte from 'esbuild-svelte'; +import { sveltePreprocess } from 'svelte-preprocess'; +import { getBuildBanner } from 'build/buildBanner'; + +const banner = getBuildBanner('Release Build', version => version); + +const build = await esbuild.build({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins, + ], + format: 'cjs', + target: 'es2018', + logLevel: 'info', + sourcemap: false, + treeShaking: true, + outfile: 'main.js', + minify: true, + metafile: true, + define: { + MB_GLOBAL_CONFIG_DEV_BUILD: 'false', + }, + plugins: [ + esbuildSvelte({ + compilerOptions: { css: 'injected', dev: false }, + preprocess: sveltePreprocess(), + filterWarnings: warning => { + // we don't want warnings from node modules that we can do nothing about + return !warning.filename?.includes('node_modules'); + }, + }), + ], +}); + +const file = Bun.file('meta.txt'); +await Bun.write(file, JSON.stringify(build.metafile, null, '\t')); + +process.exit(0); diff --git a/automation/build/esbuild.dev.config.ts b/automation/build/esbuild.dev.config.ts new file mode 100644 index 00000000..8d0e5157 --- /dev/null +++ b/automation/build/esbuild.dev.config.ts @@ -0,0 +1,65 @@ +import esbuild from 'esbuild'; +import copy from 'esbuild-plugin-copy-watch'; +import esbuildSvelte from 'esbuild-svelte'; +import { sveltePreprocess } from 'svelte-preprocess'; +import manifest from '../../manifest.json' assert { type: 'json' }; +import { getBuildBanner } from 'build/buildBanner'; + +const banner = getBuildBanner('Dev Build', _ => 'Dev Build'); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ], + format: 'cjs', + target: 'es2018', + logLevel: 'info', + sourcemap: 'inline', + treeShaking: true, + outdir: `exampleVault/.obsidian/plugins/${manifest.id}/`, + outbase: 'src', + define: { + MB_GLOBAL_CONFIG_DEV_BUILD: 'true', + }, + plugins: [ + copy({ + paths: [ + { + from: './styles.css', + to: '', + }, + { + from: './manifest.json', + to: '', + }, + ], + }), + esbuildSvelte({ + compilerOptions: { css: 'injected', dev: true }, + preprocess: sveltePreprocess(), + filterWarnings: warning => { + // we don't want warnings from node modules that we can do nothing about + return !warning.filename?.includes('node_modules'); + }, + }), + ], +}); + +await context.watch(); diff --git a/automation/fetchSchemas.ts b/automation/fetchSchemas.ts index c5c21024..f3e2d482 100644 --- a/automation/fetchSchemas.ts +++ b/automation/fetchSchemas.ts @@ -1,4 +1,4 @@ -import { $ } from './utils/shellUtils'; +import { $ } from 'utils/shellUtils'; async function fetchSchema() { // https://docs.api.jikan.moe/ diff --git a/automation/release.ts b/automation/release.ts index 9c601361..0ce8f0ef 100644 --- a/automation/release.ts +++ b/automation/release.ts @@ -1,7 +1,7 @@ -import { UserError } from './utils/utils'; -import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from './utils/versionUtils'; +import { UserError } from 'utils/utils'; +import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from 'utils/versionUtils'; import config from './config.json'; -import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from './utils/shellUtils'; +import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from 'utils/shellUtils'; async function runPreconditions(): Promise { // run preconditions diff --git a/automation/tsconfig.json b/automation/tsconfig.json index 8e1333b3..1fd339b5 100644 --- a/automation/tsconfig.json +++ b/automation/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": ".", "module": "ESNext", "target": "ESNext", "allowJs": true, @@ -7,10 +8,10 @@ "strict": true, "strictNullChecks": true, "noImplicitReturns": true, - "moduleResolution": "bundler", + "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "Es2022"], + "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"], "types": ["bun-types"], "allowSyntheticDefaultImports": true, "resolveJsonModule": true diff --git a/automation/utils/versionUtils.ts b/automation/utils/versionUtils.ts index 8ad3fa29..62aad22c 100644 --- a/automation/utils/versionUtils.ts +++ b/automation/utils/versionUtils.ts @@ -2,7 +2,7 @@ import { Parser } from '@lemons_dev/parsinom/lib/Parser'; import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; import Moment from 'moment'; -import { UserError } from './utils'; +import { UserError } from 'utils/utils'; export class Version { major: number; diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 2460bba7..00000000 --- a/bun.lock +++ /dev/null @@ -1,810 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "obsidian-media-db-plugin", - "devDependencies": { - "@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.2.1", - "iso-639-2": "^3.0.2", - "obsidian": "latest", - "openapi-fetch": "^0.17.0", - "openapi-typescript": "^7.13.0", - "prettier": "^3.8.1", - "solid-js": "^1.9.12", - "string-argv": "^0.3.2", - "tslib": "^2.8.1", - "typescript": "^6.0.2", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", - "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], - - "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], - - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.1.0", "", {}, "sha512-Jek2Drc1n6lRbr3QCB9JzK4rId5/U3rMmywiE/Quqmvq+f4unVqLeHe6XVTm6DeKn/T4Udgx/pC4Okt8FHX2sw=="], - - "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], - - "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], - - "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], - - "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], - - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.11", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg=="], - - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], - - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], - - "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], - - "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], - - "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], - - "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], - - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - - "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001786", "", {}, "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], - - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="], - - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], - - "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "iso-639-2": ["iso-639-2@3.0.2", "", {}, "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw=="], - - "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - - "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - - "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], - - "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], - - "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="], - - "openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], - - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], - - "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], - - "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], - - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - - "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], - - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], - - "vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="], - - "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], - - "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - - "vite-plugin-static-copy": ["vite-plugin-static-copy@4.0.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA=="], - - "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - - "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], - - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - } -} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..419cad865cc97c68424cbbc91486c024ac6b6702 GIT binary patch literal 211573 zcmeF4c{r6_^!JaFD47x=B10%c(PYj{LZ}pxIfZ1dR4O4fXeLb>P%4q3G*cN;G$$#V z(5y74cWvihy?5v5{5a3!kN0}7=f0ftJ$vu9KYQ)9hkH1uD|tDM@X!zqPyZkdpP&d? z&+woD@KOs{=(WJtKfp)LcVS4NN0?fK=77Eo1|vIsS$Y4t(}xw-Yeqc3l`Q6}8E>6^ zm|-qaUE1OGLE88EBx1#2{NU;o1}{_oXbhG1>(__+L}Vy~5yKBP#6ctc!+b~{9~caT zv>zxxsBfrGgilad=xVSV0O=-BK9cY4A42Rg9lS6!%qujM(U+US7zE{uDf=LgpoRY4 z42DmrXSjcWH-oP~gCPm!Z9L3&>p{gJeTC9oO4BHfrF0?a5U`s~sX3@9q{mV!L+JoY zKlfvnH&J?>($kb?f)0lITPR&gX%MBZpdwIiP3c5Rl_(XZG-P2wKzJ~NaY~rMkOlia zprn7RK&3%_L5G3bQ282E{$MKqyAXpR0p<0eGN5NchlB2+(#t{RAngW9`1({@no9o` zWY(((m51`vpd&zcf)Y+NrEZ{vZwyNOk*D(eQ|Z?N42BfsKL#b^7pxZO5e(z&HE-bp ze{YYF!%zrCG3k^V0R*}blLBcnU;IPW`~ntwG6bR1WS)fhczDAU3HAtD4D;A)o@a=M zmrp2z@r^1s1ZM~@1(eLEAJ7)zg)R*6_g0%9$_(;+xyp8f(Q{cZvUmttZ-Nk1n*PzcA{$1~gy+6V+6$Z?9w&jBTzI8f65 z4+W+_+aXQnTQG1S)G_lzeL}sU-cv>9cocW;(gT?{R&nGB!agY~dgbLGc5GZMvwH@Ry9>pyGt;}ra2`Hdr z&Vz!hF(PVA{}+1%1o(%BF?b+N;v=KZTxZY=j{pyEA7Mz7e*c1UvW`AbcGp13zR*0H zIbZIA694^xFAEy%5fbVHj>DedALj29sy>Fv5d$UlbjLF1KgNF))&EE>W_&zA$$Fnc zX$!=F@b80?_+JMl^T#JNz&|LA!APILU_fYNmV=V{JD1X?@yzvW0co;+#()y82jr9f z3hOYr(vVO3$46;9l#{fHE;EkmkS6o59@1o<80j(F9}7yxk#*i+yERac_#NOA5d-wNiQk%E`PrXTZ$&4e$sH^YLaZ@L3!|?B^RY=R+=K_Z!lK!JgHh`JrlyJpuz5 z9wC06q5lYWq<<=tnK}%VjAIcf>4(2x(83TOm{+hEJpw{u-wh7&fpnTNv)sdLVF+>f zkIwIOhY=p)537#B2o4Wg?6uI_=eP+oPFq3AezpnxB=OltDee<%AWhn@H)ATsc`@V@ zKLaU!W6q4jGf?8^49F+z0r!t7RC=T(vz=&A!VUHao2Ldri=5~0H4psu5A}(H?l2~U zJ((xBt(bB4_VEc;^ABZ|K$^^l2~@eSe~`BtY|ta7FxwNSG?<)fJ;I<;fPbKW7(?8K zS?&`W9_HT}w?*MTA&X)E^9Wb~?eR@z&WCv(p=vOeejSv3E#wnFWo?=Mc);3#EhI+S zj=3N82bF_#4U~7TD^OSxF=e16KCI)7`@>}5$Px}H>Cdle%=8mb*g|83AfNDX{^PiZ z!MgDTe@o#-;tQi62J1BB1fh%y*n;UuQDg$9TM>(g(nA65ksx z%z3!bGt}SP-y>+BD|6hxfRcW`1ttD(1SS3lE)0Y%iosYxrN_82?Qq`i1TL|U1r-M+ z=Q>eP+;?!F!Sr+{uV--^bg~I6w-u?>ks3~x*vr22Zng~GS?|I%{b=G z?9aj=FEYIuks%(z!HZ$O2hH;d@ed325AqH0_nQ~CAM8kfab4s3@?Pi_7UCb|gX?G; z_(A#=2TJxKUysnRMZgX52@m!6UCh`BCqoi{4_H)WT#ka0ahnE8>=!^iDNq~_+#fgj zQ|DD`ec}GaYKQX@_hr_77xz!xuW`LDn#+vmo&aVX1O0;7$$Wx!yf6~V+^BRID2eL=P{MfzWn@11 zEewW;GZ-&{OU4`PCxtNE#d>JB3i3()1lUW7pDK_hKu+~0t^#3#1kvPeK5`P|pii4I@S_n$Y(?Lml82?$|KZ$cZn(KN^(8{WsOm zcc8@or=TRh6`-V@GoYki@oHv%9;8XVbWkzSHK5|45un6ga}AU44QbLJJ5b`c4yCG~ zq@8at9%LOff|CA?hJJR&hZ?Vepky5JI^n%L(>}xpu4E&87^BxR_xll`#2*=|9?m~p zw-2Bm@$XY2lgBy_aQxYgFKfKJ8*d!v=Jm|@IBsC}>n$khxBW(@zvh&}IEMPboCWQ? zF~D(o0QMwqmq3aC7}pb!CgYv6i5X{%KkK~0^~O5Ca319(F}bYcE5406U;2X*e{sHi z-pXuuKhz`hdJCu^D9%5ecd+=$Wko2?H=JLr^NMvI;r!ur-du+9CHqMhC^>(pfs*;X z5tPge*ls)hoC!+O>bsf#TS1!GjRPh1VGefIx7x!jPX`r){B@wDpDOyd<(>75Ew4FB zHw&!)7OBkbFJ!^@Jt{?G_By2k{YiWa^{%u|ODiefxJrD@+k^(KuoBT)v60`V&wc&- z`$mt?)fN+<=GH#7!L+N#^~%>ayemiD*&kB7AkQ^)t!2l_g9_2^cEc4$T)LYiB(HB0 z+5cnFX`gfI*fbmd-cdEv0$-R*To{^3O5DqYq5qyjl0P{QPxk37%p<`2>fH zepVGx*5XQfy6@p?U-L2B>MmYlytPnj>0>nX`}Z*pmE(QN``d3fZw|b~D{L^Tm}|?m z1n!)9?!r1z8QII{zvWfDscD?%Uc@8VXS~g|!hM@gF1h$TC2O#_#g~`c!rJy3elGDp zrI)466ZoQ`;ry;6W6w^q4|r%PcyMxx@i~S43K@)DpY`QuE4DUACxq-A`6$6^kYH2o zn9)D0ey*CbXThuN0XEW0?6{ss--%geW>x$A+$Al;{?mLz5-Pl%8qV*F|FlxFYJy|d zi^KslQY#vYmhU-lt>_!;w))|_s}n{ZojCSKjd|m)Q2Byedvk;~D7>AQJh{({$%?$~ zCZ=Uy-BOopr&s$W$;lGi+T=qIYuP4FCGwZ+FL)m}LDDI20M{`$F9=eB6Ltjdij8pK6U5 zzFk6kghSHT@NcoxUu`P5AbOwYbm`0C`X4Wxs1zDEkK1{yQ#H@w%c>fdKfk(&wyl3O zca7l8r=>e~PrUDFC2s%v`*qD}chbg{Oo*7^E^NMg(&sS#fr~e%Jl}U`#}a|h7oI#1 zcpKgH*<5V!3mfGG%Zo4Um-L-FJ+(;j@*&5OQuDdp9=un#-7C3P@@wFm>?tD>*RN)1 zH%eAsIdHPEy-_#vsK z+!^h!<}Z!o(;2_#m4mgxt7i_c7Jn$X|IqNv@^8XE3p%u`Q|DTF4IHs{dfm<;MXi!+ z&McTCw79Xh$>h>%k?eKjZ9hxD%Mm*H$oBWMliy_OHpc8O9%A);#-L3KPjx~(-SmeZ ziJU8GHz9q-!0K1cX=#ocr8Y)GM6#PEZ8^l+4)Pm2s*fpa587eT6kw)$vFOySB?r#7 z4{1229ldTxTXbH-`?6)h>G!VPYTKTBS29|zZQ+g^g3A+Zw%fnkmpQvU+Vy8Z>946S zQSL=UFW1-9bUb^d>)}x`)|O9elh=}qyY?=N*LQ>tGt-Y8>m&H#<*~&nGn*ED zX-s$+cEYxjH*)&tuXD{sv6yQ<p$hHxG%M%@;e9Fm}flqq5t@&O;x3+2DP8@3Rl6?RHxPjOwi?j~oM(Fg;EOf9EoEI(ZXr>g!uM!R?vWuvCrXxV2-<=B#O!#vC6fw1 zU%ol;h3C@EY8NriLEk2QojTt^YG3Ee9xo98R8{h-T2Q%RX=LhuXodQ$QyNPo&W46;p@}pdFuPsct^+2SHS+v8|QvV zWAvM?7e#d2-EB5ixhm@#9U8SP2lqqVSNBhSxAM`1>{UhCO3mwPFI}s5V~Bp-WR-Bh z+kIJv%|b_?Uq^JLC#`6gSAXtcOZ$0u|A=&GUdB?k*CA**Y}qfop`qD=4Jll;UjjJJV^XvCffJjsQBXMubJ~R_gI-) zX~kD)y|kBk`));+=Nf06w=>1>hA)|5HBt1H^zFuF&y9GVw+)`&S2P0m9o%OyJ^exB zD3Re)mPn6il)u0?;?!)e^oQnO_5_P2)iq7OcKIO2zigJ~mjTIz8*u$$Tv_*n?5|4# z+SgBU_no=!t>i3Qv&&Vj1Qx;-}W1FaP*~D zO*Oasz1%uJXvZ;rT-Uh15~r(}tJRdg#dVY?F?`C!!SemjkKS3QJAJ5iM3eO`P1|*I z1QpDj47ZMUt>&M4dv?J)mH7TDyQl8Y%hVli>9%B(^^_#y`R6CpykhoGQ1s%&>m2WtqO1ZHVnmdC_yb z_S~y|oguK}RQS3QqoI42rmo9!8vFXV{yx!y>B^RdNqlRqpVaJ3t(068SUj+QddfBV z)h{xnhy7ZSa`b)qIll+)RWt?ht=P7UxW3xDVF<$i#V2IJ+uoB z77%OD)9T1O&a?UU81p2#K_4={@ksB$_6^RdQ*5pou$ z#x@_2xzJb=)jI9I`7guw{NnTH-TXdv$oeR`jtA8Zds7FDAATzNo#^dklXHiv-t?Uw znfPMcu+IaVbX%uvP8rndajc(y*r7X&NuMGzERvS?6A3HcyVE@F@EzyFSCVq$Rvsu7 zspDR!CpvkYarmVh4{xD%wNbC4RVAATH!M4U%WIN|{0eiGkFRA#a|hN1My)Bl71D9!``xBw#ql;v zMyc<4r*5{`IIZNO*`NpF+to#4e4hhE z6pwS4Mme0?W3RNtYLM91+3%B|EF1f~P};U%;%Pzd-A(RehsC(rK5=ZBrabwL_p_GI z2}^B;nXim!zTaoKfYG*Asilt2b*nE96}$dnSc=SG|%3f9J zTbk+RpOXx$7A5AVoai%X<8{XV`4QoZJ6xm(p0tyeN(~6d>ja-NoL7w2(yqLXI2a1ASIC|b=r4{-e1jW27w zyBlvDXGV>E*)NNyPs>fu-zeH(V0-J(J|}yv?G+t!Hq3l=)BN7vN7r;Y@EciwUBjuTLyW_hdh}3Q|W6+f)&p!9-J=`H#Y9GvAcaMNcr8t zCtn+E9tST-mcvOH4~2*o_eu2)R+oqjGkN=E_e?U;9-jh`bshF^_xv znW!Jio8CA)o4s+*gSI5)kzBu~sLT~o51&@0b5m18syaYAIsf(k1Dd<0{@l+6_x-(b z^r0Z~0Gt8O3;I%N%@t6 zM~}wA`lvnSqyq<@+5K5x7$11X-lGHkuLlm9hgeC1`T)V zFK|rY0=_r;wF5Z4(XWrd>5YEbz>SV$FT`gLaOU&^=O=LBtLT`2JAYh;^o~Ep!13w@ zf282c@!nWp(ZJ~qe;)UeKSuCli{2RbUBL0{g*bD=4-AZHeE$bC27xA*jU1R{}B3>;G$2g`e^$oWZeFwGjL?s!;l+As#g8vMaD z_OHA8SdR5}0tbHD9)r(o&>t*Edv=O;4}e3S|FDivPyIrUgf#QH4W`i_RCaz~KGvHK zoZcAsbu1k8gVTOp297qwhqWGS8vDVj*ss1a%=qBCKz~qK>#?ShGZi>wKI6Ef9jBZ$ z;E?B;=nryws_4&c;Mf2M({P*FdBE*>j}EL?HJrhi1RU%KJf`S8kk=gyIfG>x409TX zHBLR{%mNNs7wFGF{c`Ro+qFnk1q;S2r3d5D~z#v3_nfa6T#z;@kvV4T?bf%(XJ1f1D44*J7szs%(s z45MD)lmmyHPjP=n<#fEh17|dFu%8$gPB}^nz0$2Ml!FX&=2HcIoh*Rv~vQ^6z~V_kOz;=dUPOX4{*pl#QvZ^I1jpm zA?GP@CIE+8$jeU;}l#s}vkr{9}vjAE`+Z2X`0YdvtxY5t&} z=pXXgDcau#jsbA~?R*x2lj+=E;Di9jyB9c8YRvN&&KHaeD!aH~KE`J{aL9EpjuX~L zMSFINoFw2(1`f^}+&4JoXsa_APQbx>$VdO!`GNUpp8_2EeI@3jU3ZH16~Hk8e=v>p z@Ol%|>=f(qYjk}cl*Ep*15o+MaRZJ8Ek0Pyx3Z!~B;@1*hdhtMG#>?m$L&2j{?vmX zx3~cZ*B8d6N6tV?$ngUXS?|cf`Ge<=?wmr-QQ-Ik2kqf>)_HI`e{{z%&tK?IPwQj5 z=+8pnka@`34^HEg2^?eKAQ$~X#dg^#)@uO{xgNuQ{nI#4hjZnOUf>)74*mX1gz5lm zyt?aGKdry#4}87SdEoxYu0G~teAWZUp4Km%&uG`3BBud3wlogLxx4yUj-1i(5MU~e zgZnEg+Ot#SECvp_Zb3VI&dcdMybPQvz`^=(?%?P^e{#n$?+0;waR1?SK7Rua{Ka7X zk(4*sPIu0s-9@+v4*?EqJXq80c*xO$#Xh|kI0?XU01l2b_K#D4+JQsnA;uMXm}b`> z%*TG2z{5H#nm_!M0jK^X1BX5j(I0FF+hwO{cON+L7lU@f_@Rf29C>)CML+LgbxyB) z{eeUF4eSqcdaCHpDd0?|^$U5Nj@NhKknzIyhfp6l9WQ6yzrVl0`gs08K0C#JWdH|U z`ZEuagXQe%V?Nrw0*(c6us-TQH0(x^W39*Be^||t$C}TYMot`XOlkh09hPI7ouXYO zaNtxJ!#duWW>+8ck@FKcaEtNhIsyBG?PEUj*wx2;w2uG|i4W!@ zk5kTR;J~HbzvgrR2SW5eIOtz@iv7}on@YmLeqcVPd&-FbjwNvLeA-<-)^cpOobm_b zi*}s)!)^HY{UGx3{DWL}iuO9dA>)Pq;CUX??CN7aa>9WFUHh|dU^%D#$_I`Sjf49s zryLQ0(9fq>9~J#$r`WIm*Y?98=ofhofaBBC{$M|`Uk89g?sqVakgb9z?tD6gl641DE1|oamVpAI zFE)dmD&Ua)5IGoURJ3QO$dQ7ZZgL-oX|!ieV?L{*-BdOlR(p17SI1~o&g6!^gsH=>9~)!W?p9^ALGmEeD(wm{QcQi(N9kO$pJ`jJU?ut`i1)j_OGXk z{;R>mRkGfD8V^=Jau!gWp2mY6CmT3qUHsdAH3Fxr?-^O!WtYZ&jev)TUG0bL>ape{ zXD)DvKbS^4PS?dYii7i*(>P)K*MLKR{)0R$NB`I<+KI!L1*Xt1+@CR@T|L%(w08jx z8TWrX4>tpcKA(FUU-S$8X#kFK7vqI-LVI?K996h^r$4vB`aSI@a(sc~2LA9;4gS;b zL9FeV-+%pE&H>;!_kuq^fkV#A|JEO4d*=7}|CX~2I7YN_@98+fBe({<2bMzZ_G!#G~m$39m_eLhu48a&KJnVb;jvDBtN#H zpHFfAAs78)r)Xyj9QyV1KlNuLaA2$dvn~cu4RhKr8Tb-*GH_VG_u`b}4jeP!pdZ*@ zRCaN}e6%|N9CF^lxL};ot~*7}ci@oc2{;c`x~s@ij+}9@Na2$8&-HUp&X--Kzj zXH8>1tD@a{;E?laPupk5X#ftnj>7n`+I5#ke}=j+Ik*ncFHXnHf#P8Op3YmtD-+)z|jQ`Ykbi^)_SaI6kn2+i16zxSlnH*eSSg)t!hMd_Hhjrd@ z%Gm}SQ|K4#JY-FypR9`h)B%Uwf1y7;t&cq92zW8q1@_~gj=Lpr$a)_{H82z#>Y<`P zF~A|~9jl2_AFv$r*(utU0LKzI|90H_dNb!wUk?31|Ii;p;E?e`4r@84*(q{Dfuji= zyza$(R&IBkbHE|-!Er}>)->j`Dz?kx!}JI1akCnBlSa;X;E?zr2diN@rr9al%?A#d zhnPnDp8AFLGAItKKdkc!`K*eZI^Y;|(Jxkec4_1c^<~Bf&!6ZAtAE{bY=C0~{;Qa%tyOq;E*`?blzcoROFNahr|c{!SbGRk<-VI`R@lY9+;23?iB5(0SA6!_h%kr zKAz8d%GnPbEgDC-yNWF3*zR56jHhwXjMMqdKactR2e}xhehBPFv0fl>jA;H~emB+s z6r=qa;Fx#8;dtErnd60AY>3l-O$H9R9>e;0pMv*Kn9okJ{r}f~4|$C07y5_(v!*eh zRnhJ@aLD}yt^-*6939Bfp3j^=n8ta?>3sfwEzV)kFDr--_G17?(fPmse()@C;49P^ ztlrc0fc6+yz6D+Xy9u=GPLbmP9Bc3g(>Pu{-2td_td|ZP(=Pgj@#+qToF~AUMC0J| z2=uF`9EpJ5xo@%sjv4ra~b)*KJQZl7Yt!{-yM`K)Q=!~w^d)-SZs402(4L(l=LvA==f-RLp0m$YCu0d%a_Re|8mF^3mP^I9<&{PB~G)aq42+Ipw?n4tyo|KlW!9 z_t_?~U&c#%XS}k2L%zSjd4mmOJD6stXs5Qc>o{|I-tl9@;WW8f9xo-eKdhn#nCoKZR5e>#BE8|&RNrt5!S!0GeV<-nnz zcd$Oj8^@2GqTN&A(4R-4J(gpdoub|3m4DywU_Phgl>!`N7%vCa4)V~Rog!!Gs;=*wu$*0e%tt#X;Lyhl%Q=lt7I4V<3%UQ4^By=xv~__TPUEAw znz>F{`+?(!Ty~26hyadp7yfX1UM>SpSN-Dj{KXyH^|-V4v%7v70jH~R$9}V`kNIe~ z1vvEkI2DO#Hl}A z3C!=Qas2rIA;%s#W5FMcEAlwqe^vp9j2F%~dcIN;~Irgg*I8$gG^k*=OJ-Z}w1d_Xc{?m_LMV5Tz zm;;A?{z89P%-JT9lMEdC`2snpYyngOavFd`e~yIpQG3c6x~_Nn+2nG==&k|7kS+&+N*5o+8<8W*9zctwXbr@sRE88ZC^z{Io(%fw*K8O9Ix); zg5}t57;ubf{`^x;A#jX(fir5`-_Pq<{lzpp#eOXXPFLrPp7swprNANg1z2wo)i9^? znRol&*9q+WXP1xuH~@#fF4)<#%STQ!aOl^o?Cja)Bj+Ydzc5amu8Uzi{(c^X*WaAR zIT$$f>rC_?{lYXm#eQW0#~8*P+sFExUPraE^b6ZXE~eQj`eTs#_xeJ6EXOoEMZ4v| zA>+{eUhwSsn{ipuC z0FDE2SnIQ<+4+U-Pu$rx2g}*j$9%L)1Ws4`A(o>(J4L&j!0GC_AE)E4ncnqtKWvAc zADEAJ3Ba+Z#TmyP746w6a_$4ilg7b(9(EO3^3h&r7xOw3+sEfQMzq9t6 zpEZL`8r%N>9B1H&u-Nr<+>qn2huN=x%gF=IRPYDmi*8{%m}aMF_Zv7}oiEtc>ndLi zGRcd~K`D5VzNx{B#6}%n*y;lpI9KUc4G!WFE!A zi`0*Y7dhxk@)O`i&TYx?BK-C6A_rYbK0L-@ki8HthdX(2+1GgxCGEgzvojw~Go5Kz zR-Ff3$@(}zea)G@E^g89CRiAJnqb8 zDzSS4FVg;Vc#-rAN?(E!`xbbSgDBy@BMCT&%EL>9RANz5Pn621E6ExHZ$!nY@~)J` zL4qnLNfSQu##5MGx zRGKLHs0nYR-dHNFMKa+aO7h21X`~ttQx{~>0K$RO(<(w$-XEIf< zD<$P7R5?-d(UeLPB_GY;jreIvrL9ON9CRgF)>J-GGGFYe^#7)$eFw^pDEa6}sS~Bs zNhTb0C0R4zjnsFh>}FARM9Ih5l)6y)M9DKFA5aokf2w>wRZf)f0;v2zN`oj}2ucp3 zHkJha`Z$y_d&{jJ55R4)2K9EN&YS>zbhs4_7Id4zp^O1 z|4m6eawxn1O^L}-%8n@cc#KLDC7e7^;$H!k|Gz0QDTFtoMU;J4DhlOQP_6*_4wUrk z1E>iJc;qrYp%BN#zqI992+~ zH5%SXJvAy{lVrj{lzbcuZ=_s{$|p)bj-&F&Q~7iy{7F=~4k$6vqwMHP=7AYyXHMB! zQg(DDyeU*ZQSxyryb-@_seGbjd>yDXU5T9|l~0t|%>*U+&Qv~8(vAz2CQ8a(DV+mK z&PSe9+6$B%T`A%CQg(iz#MB?&$odW?2{uZK!r_h7j{+s>CFG4#;`ee$lW~s)CFulE zVxI)64tffd_<0VL_0$F>X`oJ0t-I zQR3G_D!-meH-VD&UV;)oTPb}5O8k3Er9Xg@_S>oSS5Ww4e5dj|NCFO`#I6s(i1H99 zr6fIoN)sjgft2!7`9w+nAW(u71SLfx@J8y3fs%4@DqoVym!Z;fRQU)htw^adrK2cS z2PN0P<3UL~6F~`I50vorDK((f5ET9x#*~_o1RQiF^~@oks0F2#poD8frKf_9hxBSt zvTr9+`AML}ZUZP8k1e3^$JkD38Yn5>4NCmoOKAqB2S7=?2SLd=9|0x(%LgUT&@X_J z`emS`{uNMibftuIm9nn_CFyHaz3ZUFzgwW>AWGVM07~rYKuNzEsdN))=llYNKSnFO zk@63q#E*|u{%25P_k&X6Eb*6{yirQ*c_0lY3_ehj9tcY634szn#Hex!Dm@&O_$5o_ z%TuZZO6n_9=~0xbgTfzUER`Nd=|oC(L5csyprpPjDEu)jseC(768D)TlSPRi&X7-Z z7FAAH!gGOq(myw<92K%U@9F=$uP2fCch4LCyRZN6zWxx*A+p}G;6>JBHoV9|l&ru1 z?(3Oxc>%E?@p%a^60a6`k%K7NZ~wcmXU0bpf<)r;-+es<0S`(Ojd6G}kB)_Zsc~VaD`9S}>uP4#y`aT~*^xu6w zS$uGi`+G8<{=2VdUf=(BU(bv~AD9&6*5tqYdS)E{yRT>N6aU@UGp~2(_w!^QC26vy zvcIqI5bPi)p58ow$HicB>1giN{`xmAthhaFsbKO*F|Der_d@2*Z*{(OE0g<;UX-h2 zqvE*DpLI7B>s(7)K5_)VN>IK<=nQf5b1T=(y|idwMEC<5mpV;!dd>ayweQ>G9s;Yb zm2cj$%$%z@O!?ab#^OJTN>Ha`gQKOTt$)Q{b>(x6M@CS@lBD=xP!n=(spP??-oWXAApJ<-XfGh?RsrV6ayDtYOy`kC6hGtxt4iu-+) zc23>(^{I=^hScv3Q^())sotBewN3o72?Q1WCD+P$aBuLP|D=B7+@P6b6uvGh(Z0{8 zm#|g(@?dS(_4ZXm-g>{ToVeO|c56#R#rBfuQx_)i9ordvQ2B{#bh(LWSccE@b+kB; zXIFS|zu zgEVJ9Ly19YC&S}L1w>xtn=W;S#wE|Z@ZdffcfVlU+u&E){e%zerH+o>IyCJ3CjRA* zoNmZ&(19h|&c+c}}IvHi;oFX&*(Y!m1pEZg)-?p312}=qb zrLNJ(YoqLpY*A6>cY@4yLI3?X_oKL`yEW<;`u@tdZl5-2ZPRk+n!rf`sVerXPyWi& zA3ID*@T|!519t-#tt=IOwZy5zwc5Momi8gLO&1=hOdY(G`5h~hi@%E^K66hCPgWJV ze$ip)yM??Cw*}qB?>oNAiBx*ykaxbVG4{Oz|G8q@%z+>IhsoFSI%wnzg_e}(#;%mQ zZ2z&TuZg6)5iJh%-$!v@z8YUw(O7WK=Cku*fr{AD-3OO0|DHPDTX@A&;q$d+wINIT z25m1|muJ7-u-0TlTYZVyPi33rMNdEPuA42BTSkuqc{Yp(_k^n{ z)ymJO-7fdSorK0dCnm+TecCu`e|WO{>4-_sr-?u8c)Y%4SN5PKA73OyO-ZQH)vnPT``T@pr?gc;TaIc5=BHvH%oDC@6Qd`x3j19G(p=DSh-`PBt>Ln|;8` z(Rzk?-Y+k2A;sXlcFp1{?KK;CEWeF+%KxxeYi^c7RNaBy1$+-X;+-A8%q0)rk(yZ5sHHJowd!qC4C>QeIhJb>x%Uamvuk)nKcR2^WU@9HwX~Q8t)CGS&uT!s|A~VrPCjaVlr^XXOLZ$IvQ|HGYxHm&Dtv*rD zBl65|@wml(h4}K9P+T&2Ll6qe-ONxL9rW>n{;Yd@ymnk?rsm2|fH$yh1-LHC6 zec#3E;}d)5>9@bWHZo0SJk6X`Q1i?BQk>tfc_DipIv4|FkP9sGoE;DD%hAJZkMXP- ztUIzsPqM>hU-s2CV&_e6Ce830>l(ay-gQltHkH{HGZQLhe;yfmb!=E)_e{ap&5Nt@ zv%amekTqXVamfr8M<^)wgy`qrT`dNTd@^DtzkNs8y?r7n&*aq&f4#iDsO&Cpp@{5P zh5jR!?iYLBFsmSWRakRl<>EQhT1CHQSH81)wXwjH;>G}LD4m;Ak#wx}`xAi<{%k+% z*Q(QQTUR`~J@~YsVru3#o!CS7a+GgqUcDK;S|;e$JG&zmHIo-=WH;?z*mq|5$2`M# zT;w}=_~VrYrUackCVjhuxy3qur}JY=avt{`8J3b(ups4~M1@q2jAO^9*Q!&m-qlb| zo;dZjRq2h2Ch-a1Rx89`ar?rbr94!nMVb1INFTD^$ZrPm;2wJ^Gx(*f&BI59BSb>K zKFI&<{^8NE?LQ({uG#*6Pf&ow9bG=}S6g|7raa+(=9uXGeD9jf{$pP^WrZ3TyC&Y$ zV*Vy4rgI&WXZv_?fA&l6aGF_Geo9|aRXEJJz`4v@IJbZPZ^7ivA10sQ*+;tiP1?*g z^IB}@3a&`WTX|1)@}*^?XN{V)I`6WNxxT#-6<^}-Foc3~e>fxl=6JtVBC^L?T`!-L zbnxw~zDLE~_K2j6MV9uDfZ=D%?j78A{gBM}%frG875Z-QF@L$v+S;aAM`(2Z*(2k* zsQ8j~Ax-Dn_+Ec_Nb{jo{~J;+PSavtg)M5Fj0}s_f@Hiw}tj-ye2HVqEO?*(w_+`?fL-h`_gMarmm>ajma-qHMp);F0xt*IZV>q4b zv?oXPqtQK_3pI5X34Jv+K2OzleH^0FcDUNZLSwFSep@60l}J!M-bmzNO(Oj$Zt#yT@??mWKS+{?y^+V4HDD6Sr=YA@k3YI55= zC*Ohk2IJ;bKx8t1gS<;u_doYG2iJ{}nTu&6ptU8)5pqb4OOKGdY(ta>U!s+RnmiH(Ce$Y}1sn`LJ1eHu=pA{PB|gL7vW)7W7`Q>cqp@ z@$LhCrkc4ZZ;sjRQINQ`G2HvbxTp=+zT8#a+RCNqI_tsX2dApuMaw4FU3TRusz|zM z_1M4q1MdNf8v{%QI(NQ<@y#cx2gfY1JlltNV*RR_D_y@`$}y6&5gB;!iOTC6*Tn{? zp6q`#df2^NgSBfekDl(>GGmfm@s(PU><4@io6{+-5ik|$+{av-c=e9igo$2%G;(HP zbFf}X-Pwck%W5>D6!>co?0Zxwaz*i068{S8q=gkh+6S(uajl3N)7NmL*@OXH%F8F- zqqro#N_6h&LpC<2e^(E0h)gJ#j22NhkKgh2dh*VfI}hBMG0JFxors#otXfCC3$csd z_S>l-vts1#p~b6t({+xyC}|iKdXwK)alu^};f|zpx4genu4bjDquw?-y^2SB?y8To zojrC9eYEe_?X8unp5A9g+{eAod#td1f`0fD?`nbB5xEz$(DrCwCx8Gwh z!CtBU+Seb0S60Rlb*I$_>pHY>g$QdX9J&}5+cx## z7ag8u4dXM9y}uzT+vxFhZAzrpf_;m5_dOYt7aC+Q>^9a^tmV{GPx5S?oR7(&O6Q(W z4^|$RUUGUL!(z0CP=J{e2ovZ$~z3P0w7yI{kF4#Hu#Esz8k|(}X91rp}j(N7? zbHS*SA0Lj?9O&Z|RJl{3U!BF?Mej^gBBn1WTA2C5?Q(6_VCuJgyky)))46kA937=I zaZB)PC-WC~g0K2IJNsC}p zJfK=}N~Avf*s7y5zQt8L54imChweJ=^wr~T#}4=?EbyU*;*x!x{MH8#?z{T0PX;W{ zE}nVVO@Md2NBXgcw_J2`MAlUly>@?AonvuUp{|&}xS)Jq%Do}y($B9M=TiPef4sic z^VU8q9;$Csptvyr)IcaGw{OS0)?;Zip9gGP5_~-)ChgQD#XH_=HqDy@Pkz{XC2_~Y z?+eE8oPVtFIV8%gNsQ~q=A$v2O_St)+M8|BuzmT1`VAg0;cC*knv-H41tz!i`e7_WLAg!t?fc%3M>em&aEAM< zLu*=&jcd}D5?K8C%Ygpkyw~!;@IXg=zx~Hch77OJUMJE2+v3OTwKlJEP40I)k^AGvmmAvdayMJ%jKc5M z$oz$Wv-IcS{&`vVXH>q?f|31%)y%hx1S*DCe^b7!Q2VaWYxB8wCKrPp3<4^xZ`{7N z*7nC3!%ZuX+|C=E-T#BBkO|L8u8~>fH@@)KxlfYcDB;0>PFuBf#V{sM^8Q;_-x4M6Z^`}W~H7R_dqz-U9+H;o3V38aDJoZ=clJ= z+zAK;<$m&Odc_)%WR?-)OaRq^$|ZY*mhCVE2W5dn~4gyo$75>R4# zKk8%n=Hi_7YK_kOGz4r!1QNXUo7X969nmy@d3HvFiT%=*0Qa{t9kQzR{wk$!kuM|% zOjxsDP$;oIyY0uOGo4)Kc}R!Oo%2w?Cib3!e29CUNnGa7XP36wuU9-gaYG+ftM^SU zA0-p+TY7)f+#2h?;7seFkD?D3Wj2?F3D`;u+MPe4;mH~!8dsOjU8?%(d7$3d8$nuD zNx_-de%X9@JGAkJZ~gk;f_1Mmm+Oypycnln|8s5PMT4P9lhV@SV)*-C@yu|VrC)ga z{)G`O6qig2Jv#T~SMmOteQhHxm(JMt_HEjZ5##Jkb9Or(Q{^338x?tabj9ZQQ(6w@ z{w4~qcMnT^A{so~MMvIar%!qEZ`%nLPUN@5hNie~@kd zVsN8h(4vpA@vk;yT=$nO>wjqctmBq; z$15x&tAYg!ORSzBIjjFc(KUa;OZ$sqn~o&MPS#R(>uVc*A!pC^=q8FA156`2*DWH= zU!KQu{kF{)qm+-<7@pX3y4g#j+;rPzw;WX;hu72OCoJ?Up2oAO_U77EI|UEOdz&c? z*WT6AsIzkWmfeoaC@vYd$#m|CV*@L%CaEoH&sg&7%_wh2N0&G4>8rDwGECnKJKE|N z=FU5mW%{D#<%X>tl9zuDZr{C{+kdZEv-9}K_j}Y;ipWmf+4jp?W6SNlo_E2l=Js+MIqhmTl$CT$DMI`x&#aG>=^Izz|^ktez}D0xV(2kmdjeEES|}& z)$k(iwz)fvYf0zwH7|dgyg2)9l~U8yed~rVk#3QB-gj7k;c2gv9?XzklRo^AiDlLA zGn2}-7lf-@$7#*~mbZOi(yYFF8YIHjYw^p|xK?y7qe<3^vHQ>n=L^3sm@hiL- zoA`e5p#ffBpEtd~`~Ax`>37~md5a$f47~3bSepLj(-AHCNv5AhZ4CJ8uQ6*sjcZNk zUT$$%Ab0igp}B(#XT&*|91Q##d$)Rf>HhPZwx$`IGzrZSTfz0Wvah)KCy$)J+{EBr z;3FOV=@K^AXxyoEu5y{%5bICoUUAaj+QYG>DzRHL>2l91yI|a)eTG>~+{!snk zmTh*U?##^nef_Fwz=Jz2nGz1Kyc5F2&+4>W(73jAZUxW1n0ME$wwl~e8lgVB`NxUJ zErX(k(kd;dNbkK?8P&Eu6rh z98LUTe>Ov3C8JHX$RKve$x52P_H^#6%|DV1w!f4cb?4f!RiDNL=4rmMnh^8)R9JkV zUt;Omrq;M3E5oLypV3Q-Mx4|(@3=oVZ`YG#CAq^nj7yde^iI&Y)9Bo%_8H49b(WU3 zZB?C9pyQ_IsCrh`bf)&3lS|7?4ZhxM_4B(h==@aeCuLKfh7Hi0WZi!8`1sxHe0-gF zW?Y$*yl6L#>pYaFxD?S?{jwlgpXfZ52qh9@pJmY zZ>O!@s+TYN!D~bKIJs5HGamo)&D0z!CxzcoF!5&)%^6U1F1aw`!0{Rr|ZQa$980?#=Ib zn;TsADcoa{Z9!t*1d-^xVKnY^I(MS&uydZ_r{rI$CSI0Zv7+>o=IO8JWv}uCbxc$3 z&-Z)cg`bIX`~^BzGfs#;HoKVh(D2l2cMJQh3k~_Ma+{;i(Z4U8LFe9>c17jN*oQ)T z4DaEWPJPnjKc^>i@2B_vM`l(ob%~YBCtUVDx5~}W&1d4_jB6I-wvM(xqxwqJz3lf* zql`(3MshTNXVSU-Ke(;@)%0;Rko%eez`%tlM(RSJ5EzP06XHt;W*E8|B(> zdafOn^~7ALRHl7$VP2A&3fHnU{}pHBwkguM&UCJL#D){g_stXxEDW0B>EiKW(BiB6 z73JF^1y*F$n~pWpb-Xgt?V!SYuK0P^AC3DJWAOS#eTdTDp*Gt`B%1qas|C=wv*_H~ zwAU%&rqYTD*_#bd@I(pEdFs8jc9W{1^XS=GhwSbO{^m7{)gRg5JyR)vO@p#fyra2w z`dVql=Y|{Icbo6_;h}M7)4AEBCa(RdrMzY9?vbu~f{Zk4&d^Nres+~$Z$7caL@ ze(dypn{%Jrxp#(K7hRc<&U6REsyhX?Z}is=8){W|(*YWUEG2Um_ef4&sx zUlc|2*OkuQzV(^+u~+YS&Z@uUDZTmLZ}cjMt5Q#;e<*t2xKW~cVVYr$qYz`h=PYx- zws$7iO;8K#+^gwF7LPfmA`h~5wGmq>uk7A>^L;^_X!7$ zJ26vckC%D7Tt4-zoy`e9ov$HF9?ucH=5AfS_J;@GC>_xQ&#U7qf`8aBzi(mglWugb zX@KVJXrXgv?(^HW76q;uddA>#?5=Cv!8@(rHcxbP8uT{R*2Aaz%*@3CO;vL}&m6IJ zXgU3r_tU(L^)5!qck7wg>rAdYo!dMs^wh~J@4$)&$E%%W6{9S@t{GcxACm{(Qkwvq#>JTWE2ZOXtoNNM7KX!&kfW z+uYL*8>fq=Y}eSs&o7Ww{?uiI+=?GNxmJrAM)Jw2CrFLakDC*{<>Q9(HNy<=>)Ynr zuUIv@gZUhs>8}T!JN4PJYVYv(QV*lQ#RU`|I`C2K`}2ZvsTEtLj>%N&C0(!at}9d& zUY2xV?q$iNBFA%$7Kq%tHFfIz&k@ajgE#mxudA6{PdZmIVZ`ox{Og9UI;)|iX_PKiQ% zUUaV6O{e8ExgTBDQk7RV9@wYEc8$S1r?6GFwM9DuzFJIJt22KGU*X95y`vo4i^k|| z<0{*b1*OZWhz~jH<#wSgJVsCYJB9|t}mV2Q92>)%^KZtPgQuWPOlTXclP}X&-MGOqU5WtR%M%gP7}>6thBSM z9%eUOe_4@X|HeHk!w&}+ULIz<%XN}lW5PTd*N@JNPEk%p`qo?{!f2S9?-}5 z{Ge0G5n-#w7S6Gc5|$XYC&(qsEvWMT$}>M>(?usAo-lQ8a-rXy_YVt-`X=?Iap%#w ztA0H&%Mfvle5%JgD6e>>`MzyN=MERUhBdAhbop8OA#~u0sh`t?Rt0{(-h9YRUHAS0 zrRW>7WvhO@v|GsO)2zV~>VglfG@kQb zHa>dM!RTYZ0^7p&QqM2xbx}UIxDr`rFNGRPc-5uipc6}Gmn&+j(-ng8NDRBAVSXmzCGa+S7n4C{KmyZ~56|hLFhx;U=9cM$ zVNI%RFf9MHXfd;_Zw|QjpiAnRL0BNh$gAa>9~~+Eg-$xBsM{fq?K{3CC0cj#iHo8! z!D`1k&%xV^{HMxC#iW&Us*amlKW{S_g*#@+T5y~>fG!NlOdCrQ(P_yNy133Tt6a`7 zmQGw|NADmf{WsXhY04s*y9~J33}FzNcg3!0A3E9625!mdCFM)Su*dg5{s7+t96{H> zG}*hchB)wgqi03!fh}m`3#r$<;b}x6-$h~urtTW!@`8|R|9DEb2F)HzBe(8_`hb7g zd>1GCN0-4Wn%9 z7T&648L6QvE-luCt;W~ElKo~bPK7yx+u?iE`gD1-dRxZF_rHw)3|sX5u62+5cUHs8 zd$-FA`rj`!LyZ03eH|lvs9LpNT5^qrphI#^9HS%hg=@$|^pC5$S%^HWn&E~%>#2yd zFkyZP96QhTkST+!Q|q5X?TJD$0M`|C+ZOn&ixAuvm zDa0Hms8tvH2S1EIME8E0a6`iuL4DJ5q$ApIAbxJX7$lIxTFs&*^WMVnYLJ%cmrfPS zg2S2h|Mpe<@A>Bbg8uglO~LkL5eP+wt%1cW>V&6gMPF=6;TiY~@g6nXR#OQTHMH0- zuRxSByQ-f&shtsdjxcBKri=e>v#?b5`^0&gD3GrQ=t>PCNpKmAWO9$#PW=70newNt z&N2jx$7!}a|5RZF-%Nx;UBB%D*VX}*s&7l-7vX!pw2T0a2)r4doP39R|hCWB@Dk@!2G9>QkS;idERtr6I5VVND z@(^D4_q*AYBrXNqf9Kh}1oUAGE;cOvLf0tD`$&D(SFqggD6=xlWYna;%+h6DakV(V zp{H3*mAoc{MK`E9<=U`$LLC!v);1e%6Pe-o4C@HE|IWpE324el^V*LeA`J{(NF=Q( zD>xP#B(cq1+{Emry-GE#1&u|Wu zP_4Nbq**&xPa8q(52mV6AvqYOCJ*}j@mw*>#pFlH&*%T^d;V{n`GT$)(>o8*b;^${ z1mE9gCJT2+pif0Sae5A0`h1Oy>`kRf{mQ)WM&Zg?`(F69wf$666I`z{grUzVyhRW< zfiQe5kgp%;QjJkv|L`l?gBpk?VS6Kqxtlz`{Ol1EwOalCYu?no5WL*o#1^gN@-l5} zA*}-Yla%T!ByE@tjTNSqE{Q*NU>*EHS6lj~L{CR_swpV%U}vlypZhhQK;{L#+$G0O z#8+q}!C|j1^03XK`ZWaA5=J)NTkKH{77%{ zkOA+X{l$#maN~;iw~=r(YWP8)6{4Nt=RKbx&jcu49g-cGrNkf9IDd!n?#P zhQN7sAn0OKY*Q`THacK`)myn$*i@}u@l;zg6%F{9(ZQqu%b4gG1PhmaS1K{^$qpJd ztwViePWYsepkn~3on1ik&HQH|-%p^s&PeH#DQt8%EaIMHrQ|d(qkA|RRwTCY(T*S! z$yYjZC}-o&CNfRZ;Py@F1^+Rf1xXZg_1`DHIYkUS8c!-CzzqW3h}Z+9ZW~C$$Aipx z8IyR5jX!mIR3|rd`iMhy^xxDT-S>*?qM4(|=pnRV16xLa*};*%bqJ4`lTSawhlHLl z0o-8FHMtwkLzC!llEBXS)ucUxaGc!l$vI}pwQz1As48x4BHr3VZ?3Ixljqvx!){TJ zJWK1%!7QJgg>02fXuKu^_WM4A?xN<&ZjIg8s3CbHEW5DpB2i=_QN=jq+ue!WV591& z0*`VNf%%0QCeCGRzI+N(hosT*8cCh-EVF14q&(;S0U+NH(A{>a>REv>q5nI9?;X>- zwon~;P&MakP^f}=bV&nYxB)}y(=Fg4?6vnN<~j4@fLC{gyLNjFOb*VD(iS}=-v9Ph z{O|u33c6(Br>c#M_WE}vR_%c?xV(DCrW9;VzSYW$x{H;O?tRXl@R&OZNL7TcH0}H zTSuNdx-W){MaCK-$l>+NUi9tf&R#gKy@+LBH@v{zhiZ&k6}hwt^_{W8{MqDTGUyp2uMAQqhCFHGj#WntHd z%skdfRdBi2BM-BZS9b`u05=kJ<>uKfZ!RyL;zoM-176*AUHyF(eS+vNbc_GYPC6nq z7T)D7J)zzvm`b)VQq$LfdHd!#b@2WW z4Z0=4wB_lSyU;MyP*BB(Boc0wt1VF-c(is-hMJVQ4y2k}wc}){-~R4udY)66mAnf0 zo#6J3jFWF&-E8Rw$HEQ2u8BqJyV}v^Ltm4dJ_v_oRUd9$Qm{l zO^rk9yydB0Hfy*w|5+Q;l60(=Fh94vy(j;=SX|(L`^5e?eq%uQ>~UHD`6`fQ0T#)( zU|HsT|J_HtWrlSkzD3k$)8Q}kn{(@#_|@y_9ee5TORJu2!;~uYA{3-6n%;+mb;GQH z{mEF+4ZT#eW(e7quZE8BF($Iwz#>_Ci2d|b)J=rL&Z8BK{)Mx?O;C<(QyYe@$;bkS zWLI!>c_4)@sI8xz2fS=HpSwT9LBn#5q zUI-xpd{|8`f&P=`qxi?ob27Uf1T??7Xq|)#o7Hpg1+cFX54!NB2oIFR1#bDzble|TSoVZ#=;4v8Ymf(Phes;(s(Gopv8NC{Tut@IX*mAe$EySR zCV=i2eFsbS-U#Qz>mA1B?BipD&jCMKuHjFiZ=Gv}Wqn`uZfQ=IF@D{=h;^q*%vZa! z^VFTe*kDTY=~-xDPh6J++(gi2kWWj?Z`F~x=z=1gDtZS<;eE@6lH zB#J2H3zZxa`!ccKiGGU=a-6>l5nR$OiMq(bqV_K(z)b>OnAyG2eeXJyaeVvFF>j=F z#O|5aonzFt*PORT$_WM;Rub0!QqnZEdQ8kf7kH>d_f#bD6u>4TwH?uvY0y1@>u<@R z>p-}w`Prv4nQ+qsMnUjUkOm+6Ks26vWQvJKU0tRtGDrr^7*j4U!f_$gMH+^<@ z=pBXjeY1{;Mg8rl50GyP=oS#nf1UqYkmx<`B<@~Bs^#``Z!+IZv^5kah&YKNV(sRS z0{y=CHmW|Ri5K$j`-3AbQT$)MN@eHp6klg?tHE`Kub|tLBE;zNZG@(*?_NAke)?ea zLQeCL?D$6z-nH*?tB?a;5D$|LarClAC)!QU%!hQov=xfOuZK|BDHDtH4&Oci`KE#{ zJe4SNdS(-;qRKlnQxmd+1Z_6P?=bLCi)Cj9@5pToG6Ii_Be;aEKeq3Y%a^SG$o3p| zSDl$gl`V_I?YiX;i9Y}hK8Mmlcc}A-sl}p>>oX=L#cU*oaPZK4 z*X1;v{@czx%bNI-Gy7zUj{^mGyPrs1*XYpt_H2qcBXnp(HP1yBDEJ8uzXSPZfbQ<@ z#MxhD?8y!&Dp53o?!{Dm?DN`Un8fkKtd_I>TzVwK?!&eR5EB24O?sENscjTqH`G`5Ibo7UI<5<2yF3! zfq^39&Q3o@4>9n$=hd#j)~~6MOBsnx$_xPV%>rFJeXLJ0vYWUQ2>U`-UDW^G-SSl` zm)-`w+oF+4uat+b>6+eEiw!8F5}#KuDYzi0D{WQllhaXi!zeq|K)(Rzec7OEgJQ&4 zmi+yfa5NQ85W8o7JCQ#p$Gq7s2Z4do*_;Eh{~XBzJ%*CU>{+Ei&fvqhu3&O%U6#Kr zPsK6YlJ}Be9dbZdV-nSdHd*~fFEx38PoZGsgBY3jMYOl@oXLT9*i3IpUirD7T4nZ- z-EAz5%c|$dJtgncTqs}36uN6$wM!!pP={R5rEc+B#WsNy;N)Os=~Zuy^m~RQr{^_o z)`xyImN~=Qb0x5x-jniM^U`&fRb&EM@I6DYlVACYSO`z?sMsgdemds}u5sSvm2bAH1l{e(!7=Dcjp&g0f0fr)Sm z)IHP*2?N|b(EUSqy0OGE4E4@0OtMheYCq(bQ1Dl|$N{vUjpCl9R6}lq#yp*yQ?Er2 zcAt544nKy|qJB0@m4}t9?ovEv0XYB72i?`+EahV?R)f~y6_M2XBl@VcV#VVxZ+0B8 z#mo+*Ccn4eQKxLwiCFu-$|iJeB#x>`wAO_t5Qe0D^9JGbXI}6=`5klxaXyo5C$6j? zizJ+8%<@wXlQe2wH6~Q>b-M^W9eIh|FQobuOZAhTlh?u=ke|=uzaJ0uZzX)};g~lF zMK+8B>QDf>Rn};OJEm+40ek&7CjmUz7~%}dsg}V_28AKKj+KX_st;VZ9ZElkza@9f31u9*_q}h-q+sBDIgiT1h|EutAcCxd+@+%xAV&X5Ps7nF6!YVnqWbIE7(kl z$L{M4ab{4%p~SI0p?n(WtHIXo@#k$c+-C!X5(i`2P!)b}O291w-7WuvhN|grvX2aD zMYxvS_n6eJj~~Y#_Kg=YQEu2mP~9sbXZCr=+kZCsdbW~_2d+WT#|?+e%?F2Y+VQ@j z0N)FXLHEAyrpXU_yE^!Uba&-&S?&=%F{vj%!fV6Ejr_3 z+gfhry$D8NDEj80rhKC{I=CKC0=fp8iMbQ@UFHYTwz$^f3y`)y95p;Iv(ce!P6yTL zcQJ_~afXCXkusFmB~#Vbj?L0Mn#^!tR z>X-tw*{{KTze`^0;KI`WUD`P?kC!Yplxd$Bu|_EUn5J5y(qBBZ^flmkMv88pm|^ZW z;Ff`|zUy=s{bqHAm36PA_+J&DB`xSle9bs2;fPyN2uFq_Y!Br~Z-jbaWaf?3ZG0)=LPy`$a? z2_WA}(9Oe$jD+$g$TcqcPMNT!n z@(da#DtAf!&hb|a8Arm%x!c!jyyH9O){3o6 zRv00}Vc~cl$|tG&CU@&+n*#2w4jO*@spW|LYF_-cg96@7`BmGd6^^$-jRqmD!aGk` zXBTd89lZv0_b-?4NytMHWA@e-$^T@}Lcv|ACe@q4eamSYswk4?RL>nPfE?^8;l<4= z`pSkM)y@1H-Fg35GdFqVAf;G50?4-(bpH>Z7j>ZffB3wp2i=|w?b<1;{^W2Ba>+M| z7s!%d3-n=V$%L>;b*LD!z81;%$SVhxZ)^z4Qo+EIBpwK`zjwC&%Ctu>$z~2aQz;JA zp#gL+Pc@2vFX8fOUTynnSA1ETEh7&j5&yAFFbU7OC{N@Q5Ts8!l^l&_KbGSS!O#pOZ&cV_a-|F{WszjyvHF+Zw9V5`P( zy7(GRp(}|!4Udg-8mll+8ps5tDRBU?|Mzg+q7)J@xww-VWd*G%uiCMrG=_3`;rKNh z*q>|$U3`>aZ31P@iM=~y0h3Zg^Mo}{oKzFZO;;te-!10P=#3pe2Vu?&=0i6Rz9-#} zKoQ7J)5fx5Iqx;55>?|HKLGi*fUaE{kYCp;A)$mV4XZ^cNgrdW|G}{g9I@`6pPd4Ay|#3cA~Ro|8#|E7b5GE)tqw_va%+K<>k_j`tn5^>KsObRZT7?W@%EQU#UKCDb)RTG zTc$1$Y212R`i{P{P`Ib!o%qxctXlUKogO^BnxDL_zbLpa(gC^^m9}>Y#{Qy^A^c*7 z_~z72JhLHYJG7b&nMkC_0Xu_vJ$|QjCdHcxhmi%)A0KZ76`QM&NCNAR$!UKhE6)W2 z`F4V?>EchHxaR1dFIXax5d(zZI4Q_$ps}i8*;#~T?ICKEQ9jpoII`0Q;OYqqyX9n! zH)R_e#X9Xyo7xkpXI1+c0B#rPa%O~os4`@3p6!b0z-D`}$i5(wHW28w%*bS0E!_M_ z_nYg6sByHif%4;w+>XpiT0Y$N&l^sh`s%x7(zNtrMZoO_-58G6_Z3%c!*-pYb%*H(OU{tUsba(uTUt*1~`%$te2;%XjnkI0!$ zbP8B@uY4&oo1dfNFm!@DBuwkCa}{@KH6-dE+Fk>7=mXvUh91%sQDa5bvqtC;!rUWg zpEI`;U9U0=bmi52WaiX?;Fu za-Y(Q&3agpE(y=T_lp70_4kUs31+(y3<$>UB%Kkx)kFH4BZ{E3j=Vp@0So(^kcGcW zj!saJcwEY8`H!QC&?CMzGC3JaQMtw#Hw1?d*ykDqU0zeM^SwrmDbX8}nM2uZf7hhk z>ciXBPfGBt%}s3Y0%?OBTtk1k+8+hOPsw=W!=$I9e&F#W)c2AI;B*;~2m3oipbHVc zH0#P{8e`|Z^BzKrYV4LQI=Fq-f>&64M42TLeIkd;QD{Y|R4aH+yaDoik#+G~Lhcmk z2Z;TQr<5qSY8T+T4TG-9CYPBj(rF(rag$gWI;Y{M-^MdNG!*D>o;V6o_rshWhcRG0 zYt%aGP&RrQYTsZUl2-ako==n0TEie3Pvi3g?g;2^3&d_6%V5Z_MsL=?HGy=dHW4&jrEL8OMvug6&HeLN_n%eDX!wZGJ%JJQWam)~{jmJSLTkD9i8h2p$T zo#13Fr>NL$0rv;!PL`M!yQ+6Iup#ze*FyX2v(>d%B#DY6@AX<8JtUu2IY+@;L7)p@ zJWNcl3el}VPfp=!#PK;MB+clsT>XgV_4bVj zKN`8sg^5XmYbz2w+iI~{ylwpAHpD7Vc+htRw-o<;7MWI=Q$ka7@V+w+x-5etk~`vJ zJkbi9VrM+aC0347`tKyrmZAR^GtCmq@UAWQpq`%_gawJ)`97FWnfod|Le1R<%JW8X96wnPmou+33j zCrAI6s&6B_5bj8#zwccUi8W(n6DyHcRe^3`REPol8b3kTkO=-vELR== zbzxBu^YPNJPCoxoF9^R{8AVUxvya1*!OhNdE^&IsTNMwU1Afy@^3o@N^=%qIg$uHi z+1*#J19kWXy3P8ryrwsqwnw8DQH29>;Y}vj3of{E>!FFER-}sItieMZn{P9!e;qZ? zz^n{g9KUk0xA{CgCYoX6QcEl4(FV9vpd0IBv&3a>GG|e?@&T3a(D25soY;)8>-+(M zmi~Q{r)Qq_A_JdDQL&F|xTS3v{{y$FRI37yMlpVO zC=@Q;9y_GWZ-=j7KKxf#llVU1&Vnv<4Y#7cs;(}wJ zQ4Q{MpK75VX&x(Z>f$$`2HA9so$@g3tpZDAIl2)!hBm?f@f_%i`^Yn8n`h(>5aX|3 zLl}!w5E{YZ6!^8*n1wi(udS7t2C8c);@y>eTas`7a7<5eMwN%M;;zHaylgGFTQH5u_S-FFlzaPcn-b#kiaQ4IpGg1PJ zShZoSA%4x#rhT1w*;Ul&f9F;F@4avVbdf1f_Chy`hZ&B!WKEsU3w>HjzH04|Yp2ym zq1I$zV1Lm!BWA-9BuAStX}jos{C>k8HbRWDKE`OvWSl!M-~r^j2)a2zm+`|1K`}on zXwr;KJHAQuDzzPFKXGHGJN(_ld^1H%*JJhT1kyK1;_Xw%26$-K$;XDH>Th~f*(nm(_w!ovOZw3G=gwnl?a zrelI3m7`TT#EiHlx-_uqu9wLGcNug`we!e7*Q@)%x6=`|O`O{CRDR59N|=i*FB-$# zE=v%jqhn*dOf6 zDOuor69ry-2CjEEG1|8N1@c`3-LM|r zDZLEo@Pq%`r|$K2!OL4OwabQ2G0UZvYCfvjrcy9uGynU(`+wtX9dsG<%sR40YD5PRM2 zPpe9~Z!9kt$UAD<_bY0;l!k+Sm`%_v*JkJ#9z*!${|zJK5D%@F!p95u)4)(9`*3xW z(($utQc+5k&{lmK`M$g>xg_)md}B3p5Xmcboli^rB&OQnxoTUW8>dTD?8(DJ=XFly z>MjjUtJU&%C~r*x5yot|)HmkOEQvNp6@9eEuz*hDih-mNujLH(Q~e*bY7JR)Uujs@ z1E3Duplb{{r;;kt^%Zh>n*S^aVH8%>>3}Xl2X0C=SYkSWa^g|dh%E(vLz=u7bFnID z%q{#E=HIy?)wn#u_MuN~QQ-V{2Xu3amxA@;-gv1sA3cR(Q4L8pWj5WBZL_uD3imRb znRd>q-LjCKszX(@37ulZo;*+5KStM)zBNlV}b@dY=vs}MTH?!Ua?0ufvd7as?1S7%_W?+_xi3zy-peum^DKzvLaeD5MhG;wr zLriCly>Jxs?9Qw9leU9hIE~s6Mq-WAB9#xtad!Y|Jq|TxfD}S5CD{Kx z0A1Ikj%B3emGUai1}X9_{O{akQ8tmPkAcjO=}vLJyzaeugKO)jY#Leb{H3zxMuT%B zcuNDFj%Nk=pkWPOF^B;99)d26JW-9fj?mTi{?m-ks|h!m6($y+qXMY1RdFNB@+>*4 zk4O`BC#W2rB4)f>JMS%(p^Sdg{5}z4s=~L-RfX#U+#}F6x_z#=aI39l3peFy<}k(z zIEC@*)7(c>vqOp@I1CJExca z!!hW(;h_!-<_akd_hQML@x$x!u%*mayT)^WgLT)pdM7lzpz=3Y@Q~m)eXd)idyv4Z zn>7rqjB=i=YB-r9O$mxlz&!z70Z#a9;&&Q+6-O{o)#$o3qmF5u8QGsal1`hMQeXnj zk+_{tiI{BSCX5jDbSp*%z8;(UJf(A|^M!tLzT4maFWLVu-&4?4(>BJZ{rD}m&OU88 zEK*5K%}@m+h>YN^;}Ze4A6poD8prK-kH6}k?wbO~ddzh6qKuNW7tfwuW^<%8Gi8ck zzwZomSGbZCa+nvPH(7}9Bb`j}lH>egZ-r_9#&~wxoiXE|!oXF1J~)kJjrf|#<6XW@ zdMxIU%fI4-7)oF|z)Oz?{*TW=moWdz+)rQtk$tek=R}soaF$_ymTk|v^6%x-O*fLBp&(v3(a2_J8Nwyj#%u(UJP)8?!E)5>7BSLpeB4 zy#!sEB}BFwL4|n=54c1bQu*$Uu60o#Q;gj+RvlD&=Fi${yeAO2x^igM0WmVt#pdSs zQ%ld)td$~m4ov2`2n|1he6K(^_!Zi3I^p9{7}=#SsU%Me;Ry|xfz(rCE9P>C(t3RS z$9NjgL>W@+C&o>1R8H9rR}N@*aaiHm1Jk6)ubOAg0rwhoVSjQY6jJoCE#!W!=9cq( zm3I}#utRXt?>wz(velLTcku(2aq2@oHJsM>h->?{zM5nIh6rMSlgW=bbcv3WfjXQ=qJddcMPRXEA8 zp@|B3%@w?cp-GuH-@*OAx1bwBzTS$dLBey!TqY0q>d%x1ITV~Xs!8L|7?FXA)w+Gs zI>rT#HQB9COya_Qa7qMer>VW)8&65!d-iPeMenl!`QCx<743>mcl>S@`Ukn6Lw0Bi zd&6AHTmys`Tt9KhhQ+kDvara^jIAVvJPyea4*F`bWcc_>hd3A9OrsC)Q&b&5_a1aH zo8lF9J$ab@k14k$$O1lJRSyIw2H-a?Z#SXN$(}TpiiA(QCB?uzrNp;z^)JqJf48s4NPw>C_GHS z<89xHwR_*0#Fn}>xr+4m&qR4HySeF8&0rL3&$a#NTL(NA4`PEvZPc9lZ!G>PKz;v$ zZoQ5zrG2kEcLwAeYuf-fZ?Eb%NYHzWXfD~S3X4m&0ZpNzGu2j>x#jB*cA?J7(k-l) zwXZ*_C5habGr2g7!U68Td;Cj4%P{8+ev!mgqX>|g91M~%HV1 z<8IQuYlEnhT^B8;Bfzz1W9#mG1#3(mkQhdtrhC@*3veMocep8J58wZar*77*$+F@XsYl0_5npL#&E}1;!1vlhS~YS zoRrI%$$PgV%y`m_;nJqWX=!?RPFVX(Bd+x{Mc{iZH0U~W@LQ;Hu>D!uY7&9nUhRB) zVzf&Bpo2hTnRI=xhX{$d)(EMO=f|wEvPveELc3al%@2#7pKYXM2ZQubT>|!%VL%tn zO6huCNVd!^_O;Q(YvU|U!ae7zzX1t)INfl*;j~Yhn`klj(3CXS3 zM_-PjyY!Zx!tkC&TVeQ{^2r|ONG6!zf%_}`d&3FyCN!bo;X>B{`pU&|A} zC`A4V-+QmjJyehQbQ|jk@e20r*84A)?ueUe$wbvBUE$k^zrzJ;D(I3vE6%9g23FvF z2Oe}MCHv0mbmPMq9Vw?zv;u}0IkK0c3Fl?*x7dg@)oPi|;jTkUoFIbWyKc%eMpv4-=hs*`ykg8 z*C=|sy$d16qQ{_u;%6w!=uUXbu*P$3RV>dyLpah-1}3myjR?AIjzY%5q}>-v0Yo0@ z<;YwG`RTs6eDYyAzRm}P38H6p!myGj(ruiDNH8N4wu>|u=lG*F)-8(9QN0}qvdjuV zzDS@ue>KD@&mftf`7nXoR}ij^pw=V?M~NKVLY(FLdiE?&WV6hL(!sG4n*P*}1q{f1a#&N~;9XIK48#erd zaFCK%3)tAIcrKckb|5Kr^UwEJ)_-a@x~L$T(QCAtkc|xkE(+)x8ois-z;CGgU>ZpM zu4irs&*Ri*i9X~z(tC{~p&ziAH=pJp=NhrHI)YNq7d#mm7V8G1#ecAP44go<$xu6j z>zk;cyZ&d>Vz7ON7a}|+XfSp<2wM=}6NmC|hqDO>%TXj6j;})kHuM*Gid@V~7T&j1 z%x*R-T-9wM`DfTtS#P3)_epfn)&E1SeG*rFw23|1 z0hz5EK}ndF`eC2hzwqSj`18b5tW-!n6pFN?&0!ZeeR@mbf;RuE>fc`D_hrcTKWREo zv4DL4eFOFq&>R!O!V9+soEvXauM?ZhaB?8!cQD?4oc(M70d-}`_m{N;vAiS}_bR9< z6`NI#_&2FkLTUUc-)h#i z_u*vxe~`WY$;}^wPTun0sdlB*b;P}=6a0*HUALZ4)W2}QJ;|67MgIHmzSQ?$?k@qI zScdag<1pX80Op-keaD!JjR-W=jdrp{x-ClBI&U!Sllo-*I`K+fe@*)cPWT`TghLOH zxT#*N>yJM!`eN>2{{b6xO+P^A@mYOseiU=+7jCrfMeVBVu}-QLOtetiM_(f;PWQyy z?8f8C{w{wYgvexO<~?q&CCX2g>vsHSzj#^%ylyz4n^LD!j&YtSsTI|;@^s}|qxnN) zu;KY}=#_Qr?zuO09;MjC(#;Nf>O1#XibGM%53H`$sh_{tX^|Ma_nUIPzXR&uCGq2jYITeimK&#BEqsZqYt&3&t8E$ z(zvfg`OBBABDqW=xiviMgg;zZIgsJw^vcuGbKrUl9_Uh=?`Ee;7z=81KO1z(B16+g zCU#sWW&VbleRO)qM1pgJL5ErPlgT+9${^`xUM_*aL5}ZLduNDiFKO^ZAJsqWWjy@* z7WpNh-P0&wv25X|yp_fh(wn*VP(!YIZn>nzYJ3ERyGVeq;lhQx z{V*G^kEiuooP34W5*O+e?3etTv%Um0LWPM#_u*^*>_?e9?%)jh0Kstf*rS0EIlb@1 zBRItlGN&k2XHR1XXhw8q^8v;Xq=WTKN^yu**3l<s?}#)xKq`BIgcTMd%%qG42Z+95Sz zgZJNmYdS9hyRYk3B+u|Ql)0xsM*S|*9Ya6usmMC)~t2PITarGtM0F>m7*@iS^Z5UhBteD@z1P;V=%@ueXzCrz}o^Ja1`T zC_dmE)Q0muBLfTXv*ge9==l&;JnzrLO_{_WYNcd!Pxq_CV=b89l-z6IU6$5$K6^gqzq zJY~@jqI80t3(C`vGOBVmaL;w!CGf0c<0zB`ROK`$6VcVs=19ejou=~Ay!z+0*;Ng0 zvb(@KP=YQ&v~S;d!cF=oUhCuVV}Ic_Z)+cks=daecsL0Y-nqc#{nSMwldrcJ#^s95 z5<;IGG1x5vLuO8*OV_8SLhQi41{LUXpJGRJ;UZfL2&hl2JF=^jTH4(wQTxm@z)*7%GNS2s|Kpm(-*VG?J@Xd0`I~&9} zOR=RB8PAfI!~0y9hxB*&A!df1zarp|N+z2BJ8Vr# zZ5wcDK(}O5*+A`=vdImDzsh?r0Zk;l%@KkuJes{)mGp7w=_);}QEq;DTKBgu!=x^M zl4aObUw8T)@cjPs{g*lGJH&s_w3qSwuV?!b&^W7PuUXS8NC0}fo@QvMZu8s z!>h+MQUM-jh3+>AbfMe{ILofTILzd^dMv07q#>+{Th`{)cS%N7T()j^XUL2Jw0F?@?FC*x3o@QZ1Ts>13 z<|B(;X4WpFMNblXim(0UVsn&ONy2ubs$^jGPd~EI`Y2UwGGGw+i2V0_t-zWiAa+H9 zT$KBt^>W>qK=Oiq-uVrWa`3~G4#SFR#q0pR_pX>V(nXHUUsTscy+m_bBtc}P< z#~tC#$BP))tm|6QaHiQR@2nCnVn8t~sSIM#v;238E7OCPaR)Pa4iO9JQl=uXroO^^ zuOw_ir=K)oJ1h^2#QH-Co@tzt<@4XgXryZq$E(x%)h|`JXq(rc7Qgo+=pv>^dJ^{q ztss>r{JWRG)PWUrGt^vvw5Tlvq)8Fcz^9My%k_|&NDQewv-Nf1OfS3lc zBn%OJB1h3s1*jNn548bzZA>*R^Jruo5hg3ytZAj_kvrmvo3Nqz7RxDi{uGC4f9lM#tvd7M z^a7nfinYG`-9{6w$d#UyD`fl8gcIO$f^Nd&fVH2niAJNYKosKDB(FOOnfC!tsbvwa z{TS>1cq0A}q_;Ff+Vhpc8F=yR%LB5XU_Op$(!XwU3r&54nAHuqT>n3JCG1bTKxq{7 z>p@)j)&sxpo(jJHQ<{`&q%e(Z@uM{^UH`O_jFb11x_&qrqvzUE{&(bXJ8Y1Ve4`Vn z)t~-7dtSx^H|X*l(~?@t?w7x@a3ub4BHhS3K~bWXe8C^pZ`m)7(TGaZrC*gWtSD1( zjK`879GFvq5@l;W%Rlo(ulw=-JnG-O;ET%xy5Cw;t+Xk=)$QZ@=A)4F{_RkYgvhdh zFT}HL;0lTlauu}qq0BP+z3#jluaAM{`JBZvC#CRc9ql)mjsKTbAFKl}=(--dPoM0_ z4~enw{P{`(Yq-8po-V4KF!~okPqECol0>@g7~bR;lKbK$KBC`+pUN*}ObyfQt7-bE z-W*?8^M7-Omwfp^cbGZ$H{%A(^8l>WM;Zr$<8=j2Q%Ody;9l6*Dl|hz!rdO!6$JSvQsDE0K} zs&6gNVSFeD+<(ucmw?VO@>9h((jG#ON2WQmgeHV5$Yr%#RzbZkShU4YBZL{s!A!j~ zyZDuGeVM#UfI6B?pSKE_ut-vmw;AX z7`z;4^qp9ZedCs*G%c(`Q$rsX^LFC|-9cX!&ICr!$L9EQb?(#Njq+yi$1m>)ty`au z*sSVR_iv7opf4r>R|IrB%9~)zFVf@sMD3z!=x&D{&!Wdq6}z!MqwmIN2GK9NwS4<| z$9huXFviQrL1RB?0=VT|n>9yN`C@V@+FcKMDDYKsezKwH!i@`99nBhCzl z8Fz;u9~q*ShKDhE*5c`xk5FmM z6jk;Kc#KE=kIl=kT7A~1xoHIz=qKFNXQEIC3}@dXxGgRs3W0}d`LPO?6!mQJvh6HlN~knJl+DwnFQ$m zAAT<)3A+D>--}3r?w&lktiWrd=Y54?*u5_XL&_*ocNRjNxL1jC@2STGnEDOdMsAQ( zA+DjVL?ZVfr}EA3A$hw?9or4eHH7FDz4n6 zW@z8p;jdW8Y_kh*6rI8z^mvK=?Qj2PHq|H@Tus~?jga*(Ne`P97JL6$FZW3q&|Rw3 zUYo)^gpp|&!A{XfI+Vza$5eMcZ`eR3F=xHaa`H+}|0RooxCTvu;x8j%ge`QraYRwn z^q_VXabO+iP!G8OuHj2S{~umAInez-yl(QKi+JiKvl2ME@QG8vJ(B!OTDfbdUEX^& z=3i>miKfnTCD6CofvaHW>8M*D*PIO>agP45>ZQD1mdv|`?k_5-dG=!#>JZL7&mMw8WM%0DB;LkjtO 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 z zqwbrGZVux)u;%boM9#t(mYlPFN-oc>=By>X;1^gXU!k=7=bM)t-)ahn3A(*RU9BQx z?iTG7Y1J6ZiIB5WQx;8DON_jKy-HrT_1EN+HEq+k4e5p)y6a?{X%lvCoa(wYIx+7? z$rjQsOOes^;UL`WQO0o}QFoqlz2fGar7pA`c_r(L3s0$LE$GDG>mQ+4)5_>$iU9d#@oqW-^*OB8|2h}-4*qj=eGzA~MS6|v+Ouu%{`w*-7 z1l@N;-NyEL1uo1@p8B<+o}1J2*IKw=kCa|l@#K1J_xa)x4$hvZVB9qHUyiEA(=z8DkAL3gaE=~grG`#O$2%0Wjj%@*vufye)i)#Lm$`|V!kE~l*dnXI;rb(v;vwtliw$^Lqr z80lk;J?TrJ>(VJsQ(FJIs&3;U4ve|;2A?pTBGh+)s9XL)+eJSlNM7w{!u%!E`5Luj z64Ij+_r8ehZn`eEH%2Y7>4qiekB=68Tj+PLs=2H9_5y$J$waq%je|5l0$59kx`RYr z4(s^0=J!|mdKW(`Z=iqZ6u={~_g&epo!d8u32&^?-tuOfzU?8d$mfsWMpgDpPJO=< zTJ_`h2D~0NV|E!8 zaA~(M4?Y@O67$PbCNSVHdMrDDQqt@6x3N z7FzqmFWz>XYGJs;;M=r)#hV0{cCKat^KU;aqf@3`Z@t?3$=m-izPnQTFU~=fgO1A~ zgo*KdYtUSQLyDV6#}}pK%o%-`chK>`+3fHibjnXdj91@eD7<#s%vm_D^lNXMWPEl@ z+K1{RryI9&@!Q;3MbQ01)V=>I-X||KtN%`Wk|gtyi{FkWwJN^t8RZjSd(?lb{IJQ6 z#pe$UIV^l19wZj^X8dJI%tP*ypY?0cSI3m)1{ob0A?RYBa?rVMeD=+G(Yg4Bnr2-c z{sMdP-PMoYzqK3`-)?9=dAv97Rk==>YPjDB_X)q7FSf_bFO^zwyTMR3bnjjRUQG)= ze}e8;qHdCnt87QDL&wkZed`}}2{lSDF)=)!!d$jp@$}VT=TVDcThTt>Pj_zD%-yhq zHL9@Uwa}+4m2v}h9P>_cX+1AUAn1+~b&Di;BkC1i&YKE}sdYNz&)ezG@1b6De2a7} z3+twe=ns+$iv!;XOPw9uX|sE(^YHaFJ7@ND%k|rW;uW^6q(2`=(EUc#UAJe|!ve{< zm6Ij*R_AZT+K?PHZ+)h5UP2SBlI^**=JKulIceEu_HoJi5qI4;TrFJb^RT!4tk<*J zs$EHJZCS*5eT=9ZQ^*;_*xeXmP$oL>?3a-*DKW)5W8W4{S6nopI~%RuJ=*yE{vxK% z2SfaICgvwJ>-@F`4LGVLGjDFMc&>W%$7Mo&zY}$vBN$e$529}2 zp(3%j)#6S@`p=jQq!Nv7Z`hv9=-N8RZA*9iRD)?*T9OX$=TUX8E18MMmv(*#yHHbD zoxr_NR@LH}{^+R##C_^GQP+TUMcz zh!DqdE{BCu<*(+kjTwjBO>vNvk?9zguXb+j47ra#!=jA038HSs>$uJr%~kC?n&=L6 z_zJDfzVviKPon;9@uyvZ1uO~`=Y4FROg10f8MIYJC}Pfcd$l`f+IQ~@{3M;0#ap^x zY?7cw854g?X~E&+=w*j~e1Bjekq%@2zrumf=4$2Y=4eYI#nX{Utnh4Tv-7m}c66~N zJp|i-rRV*x&}RQHp8@-ZhntIww>yb+209Ddf1$DP|1z%Bds8DojR3CoB$B-diL~hd z;Jku!n3JcSrwydPmXk=#{|E2&uWD0_i-tIfG@nq{-_POZstCRi@XaOo{#Ez=|GN&x zfMFGhG!LHL#>G*}$&*Av8^08Z#6__EtDgh!(F*rn4EH!EP2D5?N8=0I%9(?b;t9so z&eO%w)r&;>2%VbP*Jm!K{tq<*|Cb|x^RDIk-|t2Lmv8+qy&XQscg_EKt;6wVW#i`I z`Ul^?^yXBte`y4;-R9{0Uhn5aitp_I(i>34{>w)I`;e#IejhuJf8JOAXL@*i4tEbb zFE2+s4|PKli5kbq0Pfe`L{Yz$|MzTt>QhrA zK#c%30{>?sfNKl&phkci0cr%O5uiqZ8Ubnqs1cw>fEod61gH_9Mt~XtY6Peephkci z0cr%O5uiqZ8Ubnqs1cw>fEod61gH_9Mt~XtY6Peephkci0cr%O5uiqZ8Ubnqs1cw> zfEod61gH_9Mt~XtY6Peephkci0cr%O5uiqZ8Ubnqs1cw>fEod61gH_9Mt~XtY6Pee zphkci0cr%O5uiqZ8Ubnqs1cw>fEod61gH_9Mt~XtY6Peephkci0cr%O5uiqZ8Ubnq zs1cw>fEod61gH_9Mt~XtY6SigfqZ)WgKs2~*Sz29+_jFbo?cciE>bRTHqQ2rE_PCe z9(H!>f^sr~o{s)@ZuXLb3W8QHjt;JF_VBk)XO5}q-v^!q!yik)zn8!;z6LyGrg0_w=N zNb+^eU~44jqsS>fOB4CJqmW|VSb&>kTP&ogHxEc4+u|X`Ec`8b(jb7xG4gfrcaup2 zGZqplk$fE+oPQ+Sj)M*F0e@4L#7nlFB44)v&i%=@B=U9e_hw04WLpaPx`lAQitJA% z+c?2yK>ow`XUMiiU^67!(#ST{S_c)tn5C0#+>l>Swq=lQi@~OW6@?>{Y+C|(HLzi9 zv&c3c$g6-2w|$0xFF8RY#TkL@xl$> z+hidhM7B+X4YP893E4&iDVRy}Kp?a)9<*RXw*qj8Y@;JzrwF!8vJK}OEUN@)f(`o@ z1KFkw`9{cNUt)w5{v)XXS&+fL#0)8xT@73%+vbvOYrs}Uwy}_Hs$eT8+i-rvvTK2B zWZQhQO$}@nWE(5lrVci&3-(twNZ~)yIsof}{dEDPsJ9-#x}c4NY}0@|)&*?~$u>>M zV_ne3Nw#f(Jk|wmi^w)D$YWj5#znSmggnlBLXdKkZQ78x1Di0Ui^;Z4kWU7%&n+R_ zHbXuEz&^)Aw&_4ViELX+w&}uoHGqANmu%Yt`5Ll~k8IO}b2G?ezu|`z{v&M#jz9+c zjUd^k4|zMtW4~DjDO8oT4Om5#JoY6KvTX#2~78?VOxof@U z<%P0p55a|=6wW0(V22;jVp^Ry`l z?FM!MxL(c&n1Ok~6!>wy9EX$Gzksj6B=8+z2RjDi2PAZmPC$AM(h8swr~;~i>j18`xVF{V%2ARHJ327r%1KOh9)xWKsp*8*H4aC`~^ z{y+c_2m}Gaz#)JOz;zAh8Js80Ag+6W#c;1hfH)ulNCHv-AFv)!1=a#;fH9yAYyvg{ zynqIv2`B-|z%pPvumkXf`z(P}7uW*m0s6o;1n#{Z*a6@=y%WH-*$6NO^Z8K^U^}n_z%}s-a23b~asgbc&jGl`W&vk`(?BwSdj%f=*F$f>9S8t$-LnR8zE%Tp z?!Y+@=Ng<}ao)iB4(IOm0M4&CZ{YaX0C2w01e5@ri&O!eFE#);AK+ZB1>ktaIUMH( zc>u>9j&B@`R=`fc7T5)t062beyqW@g01IF*U$n(0Gu0k1J-~Ia1kg5 zE&(OLWuO$e0$c^kfO4P$=mUCyUZ4QT2aW>HfFs}vxB)JJJjM%<1Jr@tfC;b}&;djM zVZZ?30hR*(w7=I^Tu*WR+zprlTjxOA0^0xsU?*S*m;k#0Q@{)`2P}ZSfE8d3*aCKd zJ>UTB0~`S-z!}&NxB~8g2jB^K0p5TQa1ihX`~ZId*YF@97{ImrFmMD20dU>k0B8Y) z@H|C89)SDER3IDTk^|%d&A=U?1-J_w2V#MzKqt@z90U#kicr=RSPtw2ZbD2Xq2J^B z&;j@Ig!6BZE`oD5pcnFQfLH+6hCI+X3)~0VfCs=s;1O^Sr~_UCF92V_5bp5<%8dgP z0M7l#fCL~BcnsJ00p*ad04jl6;66|fGysi24d^ri902aU*8#f#b?~hN)&saVmIj*& zq`3c81#r)cdtBVBdID~MKd=Wd2h0F{xc>#<8c+%B2T;cd$~!?>yvIgB6Tm#KX)!=7 za12NQ5`j}d5^x$w22y}j;0%xsWB^$}HjoRP1M-0LKt50a6ap83i$F1O2`B+B16Kgt z^WYu__cXYN!9C00>T?o&|F7y)4)H4hE&!pxX^5#EIo%Iw8l2w*l7R#u35Wx#ff@j> z?*P03F97#GxYr2*>;T-md<0%Vd(=XT?XwqBOJD)ydm()VbO5-2sRsU6e!M4wbwe;; z57YvbvXm6}I&A>P{si2w1=71fD?rRsuBW8e;MxT8K8HyZE{5aZ+wXjbSd9VSfUm#^ za1zSkx+eq(0^GnNU<&M$0M6~dfS8c+e00VO~Y zPyplsIY1VW0i*#bU=<(07C8^Ahj2GG9+z<40&+Xie0b^xvb z_W%8WGhhPj0x;m%p4dLdfDvFwPSIuum;t*1ylxMGYq&XpHfsQ{u?4IEO91T_0Iu)o zM?2aOENcT`dr|C8kaqy=0Y_jT8ML_o5dhvBpMi2ef5`g*z5wOElsb8W%>%$X901$_ zH^2w*0`MLZSo$C%ktSkOQ0ra)CUc0Kn^tfI@P55zj2t;$G{WdDexTV0y+V#Kc()k zA^!?^19SsD0BU^z-U07{eqaEw0KNiW00v+f7y>>6pMZ}5wkuwTcC_I+g3p0A%#Q&0 z+!Fw5+y=0Zj04{R)EfgR{RI0CMU&zq_9IFi@ES^;Q12hF!8S)-q8;Bs@tu_cpaF35 zpN0hMNeB5k04-nx=kox1$YUFFLb?#Z{TAhY7~h5Q-If`^cVBFOY_|oFX9w5-e0DBK z@qG^8*KjY6?|hiz-W=c0_yGan3os0P28MuA0Qb6=0o?E6{=pK+2e93*0Ouj^0UQ9_ z;2hrra33oS+<^190Ht4IpX>vO;|(G&b8@^}a*ul2O_sR@Nam;On zJdQbh4@rXb6hIks%fW_rd>6%c6?|vW0`NL~$6X8DfIQY)74lg3HISn2YDkkIRe_Z9 z92jpYILG+|=L$*4O8_f@2XKyY5{EptA=U%O0?r}X0QN(CZc*^B0K@>?OXFU*80?n- z8OX~5%77Ap?+X|+c}V2|d=JAsrZ{eE0jy&ia38=}w*!xX4q!c8hwp5-U*7;|0Ghxi z0H1Xuq}l+Emt5cmfOdRld^UV8%Dkcrd7M|aKuWoO8|3kNtRuD|J}vIQvb0Tz(oOSW4p)IjAw?hR;a0ECE90IUj zK>+$t3&HC##WI+u+zX!-p8+-hR2Pc>Pi4@CZG`19kL`f*MH}&)lE*eeKcyVT5%ZM# z;qzggkCE-Dhgyk%EjfQ2QtSs5O}rlCj^_xaE~tfNvG1J$(1+(}$F@Rkq7CzC!#d(U zF!osh#u4j>3}bo;xIj*eA-xEc z0N5XJuEFtu^}Y&R0kHfh;3I(jiZWgXAwK~01H`fV0nXn8_W>MxIBwqoZviI&WBm|t z1W>yJ=mRi5y#S7fYTy>o1JnR*Kr?`2z6-$i=?2~auYp&t^s8L@p;POoHEB?yI_6n0qoyc*9xExs0FSASg%R|uR%ZF8}ryMSbxmp^HSOv z&oTdxDe7ZP@&0%{KC3@)13){aEl_tiK#6Gs}Dl;=Skp5Fuh(U0{*8`cr~IJPIo4evvVtqks z04z&+W{MBB@Hz21iZ+f@N(}J%vH##T9pn`6`IMaRg7i7i2|NQRdiZ?kL;FA4Pz!xn zC(PqLFg*rP>VW03jVU&)4_-s*FF2pzHP|n)A5zNUb*P7B(TDd&@Y*(javh%IwHPO2 z{V*=-1GRBHQ#5eAVHr$`ejM+VIuXm@GyKVicB~VQ z1st2Bz#N!Eevs1%NXN-}74o;4Cj5{G$K17hY7KQnItZk+B%A;e#b; zBt}e>W^#)-6ODzeij<6$?9A5$PVgN6WYA}tkl6_y8Fa|ad?#Q9&ysy~N;SSe#lRyc zB`YhXGV|s?7d$Nt;@7vx92rG5DS0VnnD^j5Y~X3^+$Ot#)uRnOil8O~E2pQ6qpKH; za>K&`m)P@vfM>Oo{Aww=nfGg~0mIvog+_BYeP-?>F9(IfK#dzboP&H%x`W=XpSh1L zs^R+|GkA7!cX<0N>!^T79_}Lt5x4eqv~{#{-KF+ccUgQ?ITNjmm6x3-+!^;|^P!B! zK9~7z!VmhO3_Pa-!~ow|Il$v++Oy#Pj(yhPQI?X$=(u~>d3ib7dAzWYF({R&jGMWS z3OGofZZ3|ttDHQOGgO}K)ZyL^9$6`PqTla^ETCpFuh#e8%MWTZ_mRiCc|i=U;a=LA zFAH zNyJ#U?&SNx8b5vzJWywhwaq>^XGdEr4~P0_^QG>7y>!cY@u{s{K~(peU~_R#;dr za~3?xPy>bE^A$di=h4kmSI>zAf=5;g{_pqB3O({()tnExM90Ac?ItCw_)pKnP7?>c zod>S9w{`TuC%v~xrMKi^uO5_9l2X9?c(}Q^ctaJoyFA~_c1|M^JPK0s3R0_~zgc@b zy4aG~h1E73Wc+&V_kEx5G*n&C}sbjvts)%ZYWH zX>BJ*FGutoP3zc2XFRW-lSW-lR!RE9435;bk3O`_pEJx!vl36?@U;5vF?g>T zy&!R)@2tnqYQGC!p)fJ;`mR%#E#N+K5FO}mo?d<~-k`TG!%Ci2Jsj>sQS-9%@U(-N zehD$1J}*q80c8}WWU)~_y&bGP;8vCQY@XE7Hy6*AVTP#&`*otUY21o3wbh(7gAf&d zu;6blz}xxZ;0kZgW|ek1Q2YHjGe7soF~^-G>G$&D{jGie&~i`%dBMOyK??;}D_1v1TiCH}6A)R}&=CV|3atb4*vy)WZTioB zbPnC`nL?sH*W;mcsTq`k=fU2~N&kDMc=f6-y-4o*YC0O|9uPS>(h9Q2v2ty`0>l1y zvmO<)=Y5jbNcOt1>RFF2*>mGI>p4mGMA2Q?w|BuB-&xOjvWIDbxzNCmvhY4D zImF~?VtTV@J^W;kHP}ei5ED(6s=)|nY6g-RI zJ{e18svh^-n9h3K$R0^&ZpN>TPTI4cFz|3e83XkP61;p1&d+*M$)5S#TKyTtKDTE* zC1g*LwxhYnI?=RQ&n>dY#WH8QBe7Cr*7Jhwky{XC++Ag0J?j|(56;RDv)TEY#QL|* zdYBo1&y7`fwNi=##&>2tuVK-~F{f@pTl3^I(`#B9m{?%dhF!{^=PH;fK0=wLP^Oxb zi^Kd_N(dc|f|Mf6x|_@cghy306pJnV$7bg9mHyPGdRKw+&iNvmQQn5=js|?8iig z_Qh%t>ZS%BA@FpkdkU95&blyL#)4c%S@fcc+|LKlcPP&jOfIwBKFlKY`Df@M6i+62 zum&l2UR?{$tka$KTn7(E^se&G{CClFjb}YC$Yl&))n!MH9^skwOn_$zlv&5P!`>t6 zj_s^xA#5A4t3`R75q%k@Gk?~TNmd(E6EDo~s;O+^k6l!|dNlZSz(H4{U>QRWKd8;@T@sQN1C5k~I%JFpp!I zH1OcQSBtIWyjR?VXz;+?k7J>b>_RqD36>z2o z6FI|s423trgZp^!B!UO`NGqi`|N60qb7ZE> zjG7>HU5x0}asIA^!xUb3K%2rY0C&Xp-tcuKyFJ;T zb*z3yZ6*d`PzFcInZs*4z2!f@<)m=}3-W46p6V*v4l9T$|rbz%9K#xwl(fV z?X2hTb+fgz_I7|+#D|g;x_Me|K^b_*z_G9&<|#bRUTtbGdwOg$cyOkG$o>6!{$97g z*WmA-zqi6qk>BI=Lw+<{M+bxZOs!|esUOYn+2x=2z5hFPYhC_(hjM1owE>>+8fUnV z3`R$e)C(RQbADgSo;_bz6HNBVsgRsJ?L2KDZi5E8>Vtg-gWw_0@ewP2t6A#Uuq-=j z)HUOo8CmJz!Jat2z|MI};a&=OWZ^AGR)O?K&&-(nTMZO&?mf8A%8L}rQ4pu3KgI`Y zpoNmn@BQHK{kjfn z0E6qWLSS6-NMPwJsKLz0QX;(p4@^Oacl)2c{%z5#;#to$cyPAd)Z?;o<&A5Vv!1`V z>EFk~|IP@WFaCQpu3yAa(6jm^tfw#*3h|ZE?}T88x^Q&b|A6@SS#%&RgU8 z$3=R<1FHg#)4%jlkokKL`QPb@eiFZ9&^>jpSWIDlGSqJdh+vo4| z!JlV;oGt%8Q~bTP|K6`VB!9Q*E#8Gw>HTbr;YrDT^aFTscRN^QcP`^mX8CUqt{4of zey=}n$!%=GQF^Dr179idOBM%sa8EJZ6y%=vwDmD~aIJ@y`+J-E!M{dxgwJ{-r%O6` z0)=oNLY_lfL2WUpMasNixi~)KcMn;Oqwx#52J**k{BNXZ1<}&2J-X9hJ~o#m9|7r=s#S6o|s^TmS)Q@BIH-Ycg};s+1D9d7@eecrS=kn)uP?gO)% zm5Y_F-7lBM$*p^Q_s-O9=3Cm|=iYye4m(X0=wnp=xlJLO*rX&bh!8&McfDhuW&4E5 z`!{s`xrfNW_6ehk&$F;b>cT~lD zkF}n8)IWNl2Fia~>(R3X<^*h?dB-;zE(m+JiT2;Vep!({YPXogb~4?D_XWy#v4h~j z_iesJU00i|6VP`koIWQj7a!83r6GUr zC5eFt*Hin>b)`nDRNl^dG{7SWp61s313cR;q-H%<;DL?6;R3#w&F!Td;fp3k?H|9# zv(tpbwF}_d`!KuW+Xv-aG&@Zacv!(=<>UiC_-(RnCt26O(AAh}ZCUu(2;YdUT%5tv z)_X)okIU>Cc<@~Y`oSOP_?fTacAoHc-41>)fP148S51^0l!0&VI3M6YBlQ5!fWfUh z&+PJ?zyo6u)$BYEc-wjSkvinReVBhm%nr-^-e22!dV4v#c#?cqzF8Vr-vsYwzsD(j z^>B2xT?HSBS>HLb@|5lHoKc&p+h5+m;66CIaO?@t_E_u~8p7QZ?(m^cD}x84!$rTF zPoFRJ4lND&#~36HvPaNIx5R@}7vDZ)et*N?0v>!b+8*pu^WyTA3*f>10a*X}Ep%r5 zz{foY_W|LU?aeA9!0tpt_(=kG`m>zP^~hcdXs zU(%e&{4%_x2+Ck|VC-a*J&V{cSNk7x{k;Ot)Sv`B>`=xoxc{KX?(Xw5WoB0Ada_5( z<;vHVwtWuZ!5>XQtsj7g1Il>nIU8AS2nnC58~(tC)B~P{;CWx{m$<9vz<2P#T7sT$ z;K7;vmCzE7xoWJmGiBh@x-5wq-W}01+9kXwyP$U+Sq);qNA|>Lml{n5wc*dtU-(S5fRgi6R0*LI?!eLj>8*WSLAzAPECX z*uy3eL=x`YxifQPq%p6ojh!V(ryAhIZ;EKlP5 zRdt_p`kdQ!`p$)q_y2$ICHZpC>F%nouCA`Gs$R}1M{c(7qg(&&SCA?{r+%%#*GLu~ zw1GDCLjBl&29_;Z&$8@nxSQhjJ+Du`u6Dz!H$VZ&DAr|!5e%y3QSz1Dl_hk*bEl4* zvGxc10ULQnXytRrA=x?lwzt;0WYwEHa7#I7(sZR|jym*-gN}XY-j_5Fz_$+M(3rdT ztdlkvGyKVc;fT63+bkX!RfFzn$z}Qqq^sIKzuiIm9e50Rq11DrTIRv(8LFON-Fx!6 zd-Ytm9aF$k^{&pDdZBmz*gHp|f4-$uRf`OB>r9314?E5~rT?Yw5x}OUYC&eOk7!%- z+P3e$SKXL=83YGuYkGx|Q`SoPkf$A;xR66lzW9MA{;c5T=5^`vqg6bWSLoI#d{m$p-ZS?uc+#b$Z zfgGAK{_=%wzPR4Ex1-riN7+*8Vwps7<$FKq`t7B!kUqdFAUUWP@1RbW z)v(|KM*ZH3qmQlk=gt$)^Cc6(HvbGdgr)e{VM3i(YsjfA)LJ4t{4AQ_a0yN>zJ349UkTx2<{du<67D zn$1ALm-kh=s$=69F52z3&%(ly*7t(EDenM*f<3t?l@2bOs60G^54tIA&&*v zdQ%k-y0quK=X!s;{+ws;V;H9GQNU2obsqnv86E45n2H=d>bIG#W%*3i9v&JM_- z*zK3Qe)Q8t3vR{yhrr^^KIn7h9)ce^G7Hx*4sYss`0zLc>!{!Dx1PGV<6^0X3Z$TknWw)^GMf8 zr_U#wkGw`OpUrd&-F~CIyOfR z`GBil$}T(Ty49~E2c1A9?4)zXwf^a_j;`T*D^G7S=F@4&A&y?rHSe33A6PsbFk1JX zj~tq_G9UeHw=JigOm>xyTh@ZDor^jD92TJbSeAOODz656cJI*Q`c8G-JT3 zc?&tz(i>~1c+;P`R<)G*X7O0`Ec5U&$|ir|%zKWyea&xw$~*u}d&|k+P%ZuHer>1U z+xF@q!-uuEPT>ta`C=XcSL{H4?fsw5|LM~GD0Vw(8cF=-=ds0jvmKhKmqNlL`&ZS49V!-U%0-u?N4r5i5zWly@wnc zjc+{ttrfp~<=f;P(*6TbFmbNR+3KygPrkbIv^inqv_6islkSDZ^Hk5Rm@@XRSKdA0 ztH4G>4isFB9OCH4oBnv0O@==>6FFp)!wLmpAOrkhNS20~JXA7A#j@WUQ z_sE@Z0fs^gpzW`~MzNI4nH%kX?)|^&L=LTZg0}UJQxdY?uMggG^Ch{Of}sEc?9pO> z0q5)XShUXf_kZyb&SBZv3NX~tW%K)LgKLi2Qo&#=q_jW8Zd(#}Ac?yG+bCfB)j7W$ zzW(BeT68^Aamr2CGkyE3Cyv}}@ny&%xdyfakwX^o&SOgFedF*!YANXhjCzZ8t5?*H z2F#{_DRrFmv)fPJeFKdRJUCw0Gvk>%-n(SvW#2 `d8-r6`BWx>$@iz}n&tZ`-5 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 literal 0 HcmV?d00001 diff --git a/manifest-beta.json b/manifest-beta.json index de49a262..e93a0224 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260406T152718", + "version": "0.8.0-canary.20260129T112027", "minAppVersion": "1.5.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", diff --git a/package.json b/package.json index 815c67a1..92d08d1c 100644 --- a/package.json +++ b/package.json @@ -22,25 +22,27 @@ "author": "Moritz Jung", "license": "GPL-3.0", "devDependencies": { - "@happy-dom/global-registrator": "^20.8.9", - "@lemons_dev/parsinom": "^0.1.0", - "@types/bun": "^1.3.11", - "eslint": "^9.39.4", + "@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", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-only-warn": "^1.2.1", + "eslint-plugin-only-warn": "^1.1.0", "iso-639-2": "^3.0.2", "obsidian": "latest", - "openapi-fetch": "^0.17.0", - "openapi-typescript": "^7.13.0", + "openapi-fetch": "^0.14.1", + "openapi-typescript": "^7.10.1", "prettier": "^3.8.1", - "solid-js": "^1.9.12", + "solid-js": "^1.9.3", "string-argv": "^0.3.2", "tslib": "^2.8.1", - "typescript": "^6.0.2", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^6.0.5", "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1" + "vite-plugin-solid": "^2.11.0", + "vite-plugin-static-copy": "^3.2.0" } } diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 00dca1a5..741c50e4 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.includes(api.apiName)) + .filter(api => apisToQuery.contains(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 await api.getById(id); + return 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 bddddb76..07c20982 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,16 +22,12 @@ 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`, }, }); @@ -71,16 +67,12 @@ 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`, }, }); diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index da7e7adc..53d98207 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,12 +24,8 @@ 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=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const searchUrl = `${this.apiUrl}/search/?api_key=${this.plugin.settings.ComicVineKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -59,12 +55,8 @@ 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=${key}&format=json`; + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.ComicVineKey}&format=json`; const fetchData = await requestUrl({ url: searchUrl, }); diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 6d6d1ea4..76e451b2 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,9 +23,8 @@ 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 (!key) { + if (!this.plugin.settings.GiantBombKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -33,7 +32,7 @@ export class GiantBombAPI extends APIModel { const response = await client.GET('/games', { params: { query: { - api_key: key, + api_key: this.plugin.settings.GiantBombKey, filter: `name:${title}`, format: 'json', limit: 20, @@ -74,9 +73,8 @@ 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 (!key) { + if (!this.plugin.settings.GiantBombKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -87,7 +85,7 @@ export class GiantBombAPI extends APIModel { guid: id, }, query: { - api_key: key, + api_key: this.plugin.settings.GiantBombKey, format: 'json', }, }, diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 14094d0b..57a42f1e 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -5,40 +5,16 @@ import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; -interface IGDBCover { - url: string; -} - -interface IGDBGenre { - name: string; -} - -interface IGDBCompany { - name: string; -} - -interface IGDBInvolvedCompany { - company: IGDBCompany; - developer: boolean; - publisher: boolean; -} - +interface IGDBCover { url: string; } +interface IGDBGenre { name: string; } +interface IGDBCompany { name: string; } +interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } interface IGDBGame { - id: number; - name: string; - cover?: IGDBCover; - first_release_date?: number; - summary?: string; - total_rating?: number; - url?: string; - genres?: IGDBGenre[]; - involved_companies?: IGDBInvolvedCompany[]; -} - -interface TwitchAuthResponse { - access_token: string; - expires_in: number; + id: number; name: string; cover?: IGDBCover; first_release_date?: number; + summary?: string; total_rating?: number; url?: string; + genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; } +interface TwitchAuthResponse { access_token: string; expires_in: number; } export class IGDBAPI extends APIModel { plugin: MediaDbPlugin; @@ -59,72 +35,58 @@ export class IGDBAPI extends APIModel { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); - - if (!clientId || !clientSecret) { + if (!this.plugin.settings.IGDBClientId || !this.plugin.settings.IGDBClientSecret) { throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); } console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, + url: `https://id.twitch.tv/oauth2/token?client_id=${this.plugin.settings.IGDBClientId}&client_secret=${this.plugin.settings.IGDBClientSecret}&grant_type=client_credentials`, method: 'POST', }); if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); const data = response.json as TwitchAuthResponse; this.accessToken = data.access_token; - this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; + this.tokenExpiry = currentTime + (data.expires_in * 1000) - 60000; return this.accessToken; } async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); const token = await this.getAuthToken(); const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + url: `${this.apiUrl}/games`, method: 'POST', + headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; return data.map(result => { const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - id: result.id.toString(), - image: image, + type: MediaType.Game, title: result.name, englishTitle: result.name, year: year, + dataSource: this.apiName, id: result.id.toString(), image: image }); }); } async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); const token = await this.getAuthToken(); const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + url: `${this.apiUrl}/games`, method: 'POST', + headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); const result = data[0]; - + const developers: string[] = []; const publishers: string[] = []; result.involved_companies?.forEach(c => { @@ -135,25 +97,15 @@ export class IGDBAPI extends APIModel { const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, + type: MediaType.Game, title: result.name, englishTitle: result.name, year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.url, - id: result.id.toString(), - developers: developers, - publishers: publishers, - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.total_rating, - image: image, - released: true, + dataSource: this.apiName, url: result.url, id: result.id.toString(), + developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], + onlineRating: result.total_rating, image: image, released: true, releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', userData: { played: false, personalRating: 0 }, }); } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; - } -} + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; } +} \ No newline at end of file diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index 3083e234..8d0e34b6 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 2d39a4fa..83d47322 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 a8972b3b..a88eec94 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -27,13 +27,12 @@ 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 (!key) { + if (!this.plugin.settings.MobyGamesKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${key}`; + const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -71,13 +70,12 @@ 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 (!key) { + if (!this.plugin.settings.MobyGamesKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${key}`; + const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${this.plugin.settings.MobyGamesKey}`; const fetchData = await requestUrl({ url: searchUrl, }); diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 38620807..51b4f17a 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -76,14 +76,13 @@ 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 (!key) { + if (!this.plugin.settings.OMDbKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const response = await requestUrl({ - url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`, + url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`, method: 'GET', }); @@ -161,14 +160,13 @@ 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 (!key) { + if (!this.plugin.settings.OMDbKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const response = await requestUrl({ - url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`, + url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`, method: 'GET', }); diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts index f42f7918..645d15ba 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/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index ec0a53ea..1c79e5ae 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -6,22 +6,11 @@ import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; interface RAWGGame { - id: number; - name: string; - released?: string; - background_image?: string; - name_original?: string; - website?: string; - slug?: string; - metacritic?: number; - developers?: { name: string }[]; - publishers?: { name: string }[]; - genres?: { name: string }[]; -} - -interface RAWGSearchResponse { - results: RAWGGame[]; + id: number; name: string; released?: string; background_image?: string; + name_original?: string; website?: string; slug?: string; metacritic?: number; + developers?: { name: string }[]; publishers?: { name: string }[]; genres?: { name: string }[]; } +interface RAWGSearchResponse { results: RAWGGame[]; } export class RAWGAPI extends APIModel { plugin: MediaDbPlugin; @@ -37,69 +26,40 @@ export class RAWGAPI extends APIModel { } async searchByTitle(title: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - + if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, + url: `${this.apiUrl}/games?key=${this.plugin.settings.RAWGAPIKey}&search=${encodeURIComponent(title)}&page_size=20`, method: 'GET', }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } + if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); const data = response.json as RAWGSearchResponse; - return data.results.map( - result => - new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - id: result.id.toString(), - image: result.background_image, - }), - ); + return data.results.map(result => new GameModel({ + type: MediaType.Game, title: result.name, englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, id: result.id.toString(), image: result.background_image + })); } async getById(id: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - + if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${key}`, + url: `${this.apiUrl}/games/${id}?key=${this.plugin.settings.RAWGAPIKey}`, method: 'GET', }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } + if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); const result = response.json as RAWGGame; return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name_original ?? result.name, + type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.website ?? `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), - developers: result.developers?.map(d => d.name) ?? [], - publishers: result.publishers?.map(p => p.name) ?? [], - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.metacritic, - image: result.background_image, - released: result.released != null, - releaseDate: result.released, + dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], + publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], + onlineRating: result.metacritic, image: result.background_image, + released: result.released != null, releaseDate: result.released, userData: { played: false, personalRating: 0 }, }); } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; - } -} + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; } +} \ No newline at end of file diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 4f4e1741..936ab57e 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -1,3 +1,5 @@ +/* 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'; @@ -6,36 +8,6 @@ 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; @@ -55,16 +27,15 @@ 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 (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { query: { @@ -114,16 +85,15 @@ 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 (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { path: { movie_id: parseInt(id) }, @@ -147,7 +117,6 @@ 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', @@ -160,14 +129,18 @@ export class TMDBMovieAPI extends APIModel { id: result.id.toString(), plot: result.overview ?? '', - 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) ?? [], + genres: result.genres?.map((g: any) => g.name) ?? [], + // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type + // @ts-ignore + writer: result.credits.crew?.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [], + // @ts-ignore + director: result.credits.crew?.filter((c: any) => c.job === 'Director').map((c: any) => c.name) ?? [], + studio: result.production_companies?.map((s: any) => s.name) ?? [], duration: result.runtime?.toString() ?? 'unknown', onlineRating: result.vote_average, - actors: getTopCastNames(credits, 5), + // @ts-ignore + actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [], image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, released: ['Released'].includes(result.status!), diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 09258031..557eb4e6 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -1,3 +1,5 @@ +/* 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'; @@ -6,40 +8,6 @@ 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; @@ -59,16 +27,15 @@ 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 (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { query: { @@ -93,32 +60,36 @@ export class TMDBSeasonAPI extends APIModel { return []; } - const topResults = searchData.results.slice(0, 20); - - 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, - }); - - if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) { - totalSeasons = detailsResponse.data.seasons.length; - } - } catch { - // Ignore detail errors and use 0 as fallback. + const ret: MediaTypeModel[] = []; + + for (const result of searchData.results) { + if (ret.length >= 20) break; + + // Fetch series details to get the total number of seasons + let totalSeasons = 0; + try { + const 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; } } + } catch { + // Ignore errors and assume 0 seasons + } - return new SeasonModel({ + ret.push( + new SeasonModel({ title: `${result.name ?? result.original_name ?? ''}`, englishTitle: result.name ?? result.original_name ?? '', year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', @@ -127,25 +98,26 @@ export class TMDBSeasonAPI extends APIModel { seasonTitle: result.name ?? result.original_name ?? '', seasonNumber: totalSeasons, image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', - }); - }), - ); + }), + ); + } + + return ret; } // Fetch all seasons for a given series async getSeasonsForSeries(tvId: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - if (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { - path: { series_id: Number.parseInt(tvId, 10) }, + path: { series_id: parseInt(tvId) }, }, fetch: fetch, }); @@ -188,9 +160,8 @@ 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 (!key) { + if (!this.plugin.settings.TMDBKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -200,20 +171,20 @@ export class TMDBSeasonAPI extends APIModel { throw Error(`MDB | Invalid season id "${id}". Expected format "/season/".`); } - const tvId = Number.parseInt(m[1], 10); - const seasonNumber = Number.parseInt(m[2], 10); + const tvId = m[1]; + const seasonNumber = m[2]; 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { path: { - series_id: tvId, - season_number: seasonNumber, + series_id: parseInt(tvId), + season_number: parseInt(seasonNumber), }, }, fetch: fetch, @@ -234,10 +205,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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { - path: { series_id: tvId }, + path: { series_id: parseInt(tvId) }, query: { append_to_response: 'credits', }, @@ -269,28 +240,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.toString()}/season/${seasonData.season_number}`, - id: `${tvId.toString()}/season/${seasonData.season_number}`, + url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`, + id: `${tvId}/season/${seasonData.season_number}`, seasonTitle: seasonData.name ?? titleText, - seasonNumber: seasonData.season_number ?? seasonNumber, + seasonNumber: seasonData.season_number ?? Number(seasonNumber), episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', - airedTo: formattedAiredTo, + airedTo: airedTo, plot: seasonData.overview ?? '', image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', - genres: extractNames(seriesData.genres), - writer: extractNames(seriesData.created_by), - studio: extractNames(seriesData.production_companies), + 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 !== '') ?? [], duration: seriesData.episode_run_time?.[0]?.toString() ?? '', onlineRating: seasonData.vote_average ?? 0, - actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits), + // @ts-ignore - append_to_response credits not reflected in base schema + actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [], released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), streamingServices: [], airing: ['Returning Series'].includes(seriesData.status ?? ''), diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index cb6ce28c..1c8a21fa 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -1,3 +1,5 @@ +/* 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'; @@ -6,27 +8,6 @@ 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; @@ -47,15 +28,14 @@ export class TMDBSeriesAPI 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 (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { query: { @@ -105,16 +85,15 @@ 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 (!key) { + if (!this.plugin.settings.TMDBKey) { 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 ${key}`, + Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, }, params: { path: { series_id: parseInt(id) }, @@ -138,7 +117,6 @@ 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', @@ -150,13 +128,15 @@ export class TMDBSeriesAPI extends APIModel { id: result.id.toString(), plot: result.overview ?? '', - 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) ?? [], + 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) ?? [], episodes: result.number_of_episodes, duration: result.episode_run_time?.[0]?.toString() ?? 'unknown', onlineRating: result.vote_average, - actors: getTopCastNames(credits, 5), + // 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) ?? [], 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 8a83e92c..520516f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,19 @@ 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'; import { GiantBombAPI } from './api/apis/GiantBombAPI'; import { IGDBAPI } from './api/apis/IGDBAPI'; +import { RAWGAPI } from './api/apis/RAWGAPI'; import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; import { SteamAPI } from './api/apis/SteamAPI'; import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; @@ -30,7 +31,6 @@ 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'; @@ -222,7 +222,7 @@ export default class MediaDbPlugin extends Plugin { } // filter the results - apiSearchResults = apiSearchResults.filter(x => types.includes(x.type)); + apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); if (apiSearchResults.length === 0) { new Notice('No results found for the selected types.'); @@ -377,22 +377,28 @@ export default class MediaDbPlugin extends Plugin { return; } - const selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; - if (selectResults.length < 1) { - return; - } + let selectResults: MediaTypeModel[]; + const proceed: boolean = false; - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; + 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; } - await this.createMediaDbNotes(selectResults); + await this.createMediaDbNotes(selectResults!); } async createEntryWithIdSearchModal(): Promise { @@ -554,7 +560,8 @@ export default class MediaDbPlugin extends Plugin { } const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); - fileMetadata = { ...attachFileMetadata, ...fileMetadata }; + // TODO: better object merging + fileMetadata = Object.assign(attachFileMetadata, fileMetadata); let attachFileContent: string = await this.app.vault.read(fileToAttach); const regExp = new RegExp(this.frontMatterRexExpPattern); @@ -571,7 +578,8 @@ export default class MediaDbPlugin extends Plugin { } const templateMetadata = this.getMetaDataFromFileContent(template); - fileMetadata = { ...templateMetadata, ...fileMetadata }; + // TODO: better object merging + fileMetadata = Object.assign(templateMetadata, fileMetadata); const regExp = new RegExp(this.frontMatterRexExpPattern); const attachFileContent = template.replace(regExp, ''); @@ -705,20 +713,6 @@ 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 || [], @@ -729,8 +723,6 @@ 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 0763d32e..1aee8eeb 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 '../api/APIModel'; +import type { APIModel } from 'src/api/APIModel'; +import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper'; 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 1efd66a1..931556c1 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 '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type MediaDbPlugin from 'src/main'; +import type { MediaTypeModel } from 'src/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 800eacee..986cb8a8 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.includes(mediaType)); + apiToggleComponent.setValue(this.selectedTypes.contains(mediaType)); if (apiToggleComponent.getValue()) { currentToggle = apiToggleComponent; } diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index b34f245d..bf08d32b 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,7 +21,9 @@ export class PropertyMapper { return obj; } - if (!MEDIA_TYPES.includes(obj.type as MediaType)) { + // console.log(obj.type); + + if (MEDIA_TYPES.filter(x => x.toString() == obj.type).length < 1) { return obj; } @@ -31,32 +33,33 @@ 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)) { - 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)); + 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; } } - - if (propertyMapping.mapping === PropertyMappingOption.Map) { - newObj[propertyMapping.newProperty] = finalValue; - } else if (propertyMapping.mapping === PropertyMappingOption.Default) { - newObj[key] = finalValue; - } } return newObj; @@ -77,28 +80,34 @@ export class PropertyMapper { obj.type = 'comicManga'; console.debug(`MDB | updated metadata type`, obj.type); } - if (!MEDIA_TYPES.includes(obj.type as MediaType)) { + if (MEDIA_TYPES.contains(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 = { ...obj }; + const originalObj: Record = {}; - for (const [key, value] of Object.entries(obj)) { - const normalProperty = propertyMappingByOriginal.get(key); - if (normalProperty) { - originalObj[key] = value; - continue; + 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; + } } - const mappedProperty = propertyMappingByMapped.get(key); - if (mappedProperty) { - originalObj[mappedProperty.property] = value; - delete originalObj[key]; + // 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; + } } } diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index c31bdbf9..d9a90173 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -96,7 +96,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode N/A} + fallback={—} >
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c7e694ab..f838410a 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,9 +1,9 @@ import type { App } from 'obsidian'; -import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian'; +import { Notice, PluginSettingTab, 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,16 +14,15 @@ import { FolderSuggest } from './suggesters/FolderSuggest'; // MARK: Settings export interface MediaDbPluginSettings { - OMDbKeyId: string; - TMDBKeyId: string; - MobyGamesKeyId: string; - GiantBombKeyId: string; + OMDbKey: string; + TMDBKey: string; + MobyGamesKey: string; + GiantBombKey: string; IGDBClientId: string; IGDBClientSecret: string; - RAWGAPIKeyId: string; - ComicVineKeyId: string; - BoardgameGeekKeyId: string; - + RAWGAPIKey: string; + ComicVineKey: string; + BoardgameGeekKey: string; sfwFilter: boolean; templates: boolean; customDateFormat: string; @@ -273,16 +272,15 @@ class MediaTypeMappedSettings { // MARK: Defaults const DEFAULT_SETTINGS: MediaDbPluginSettings = { - OMDbKeyId: '', - TMDBKeyId: '', - MobyGamesKeyId: '', - GiantBombKeyId: '', + OMDbKey: '', + TMDBKey: '', + MobyGamesKey: '', + GiantBombKey: '', IGDBClientId: '', IGDBClientSecret: '', - RAWGAPIKeyId: '', - ComicVineKeyId: '', - BoardgameGeekKeyId: '', - + RAWGAPIKey: '', + ComicVineKey: '', + BoardgameGeekKey: '', sfwFilter: true, templates: true, customDateFormat: 'L', @@ -373,7 +371,7 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings key, '', PropertyMappingOption.Default, - lockedPropertyMappings.includes(key), + lockedPropertyMappings.contains(key), false, // wikilink default ), ); @@ -546,31 +544,37 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('OMDb API key') .setDesc('API key for "www.omdbapi.com".') - .addComponent(el => { - const component = new SecretComponent(this.app, el); + // .addComponent((el) => { + // let component = new SecretComponent(this.app, el); - component.setValue(this.plugin.settings.OMDbKeyId).onChange(data => { - this.plugin.settings.OMDbKeyId = data; - void this.plugin.saveSettings(); - }); + // component.setValue(this.plugin.settings.OMDbKey).onChange(data => { + // this.plugin.settings.OMDbKey = data; + // void this.plugin.saveSettings(); + // }); - return component; + // return component; + // }) + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.OMDbKey) + .onChange(data => { + this.plugin.settings.OMDbKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( setting => void setting - .setName('TMDB API key') + .setName('TMDB API Token') .setDesc('API Read Access Token for "https://www.themoviedb.org".') - .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; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.TMDBKey) + .onChange(data => { + this.plugin.settings.TMDBKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -578,15 +582,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('Moby Games key') .setDesc('API key for "www.mobygames.com".') - .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; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.MobyGamesKey) + .onChange(data => { + this.plugin.settings.MobyGamesKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -594,15 +596,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('Giant Bomb Key') .setDesc('API key for "www.giantbomb.com".') - .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; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.GiantBombKey) + .onChange(data => { + this.plugin.settings.GiantBombKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -610,15 +610,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('IGDB Client ID') .setDesc('Client ID for IGDB API (Required for Twitch OAuth).') - .addComponent(el => { - const component = new SecretComponent(this.app, el); - - component.setValue(this.plugin.settings.IGDBClientId).onChange(data => { - this.plugin.settings.IGDBClientId = data; - void this.plugin.saveSettings(); - }); - - return component; + .addText(cb => { + cb.setPlaceholder('Client ID') + .setValue(this.plugin.settings.IGDBClientId) + .onChange(data => { + this.plugin.settings.IGDBClientId = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -626,15 +624,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('IGDB Client Secret') .setDesc('Client Secret for IGDB API.') - .addComponent(el => { - const component = new SecretComponent(this.app, el); - - component.setValue(this.plugin.settings.IGDBClientSecret).onChange(data => { - this.plugin.settings.IGDBClientSecret = data; - void this.plugin.saveSettings(); - }); - - return component; + .addText(cb => { + cb.setPlaceholder('Client Secret') + .setValue(this.plugin.settings.IGDBClientSecret) + .onChange(data => { + this.plugin.settings.IGDBClientSecret = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -642,15 +638,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('RAWG API Key') .setDesc('API key for "rawg.io".') - .addComponent(el => { - const component = new SecretComponent(this.app, el); - - component.setValue(this.plugin.settings.RAWGAPIKeyId).onChange(data => { - this.plugin.settings.RAWGAPIKeyId = data; - void this.plugin.saveSettings(); - }); - - return component; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.RAWGAPIKey) + .onChange(data => { + this.plugin.settings.RAWGAPIKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -658,15 +652,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('Comic Vine Key') .setDesc('API key for "www.comicvine.gamespot.com".') - .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; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.ComicVineKey) + .onChange(data => { + this.plugin.settings.ComicVineKey = data; + void this.plugin.saveSettings(); + }); }), ); apiKeyGroup.addSetting( @@ -674,15 +666,13 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('Boardgame Geek Key') .setDesc('API key for "www.boardgamegeek.com".') - .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; + .addText(cb => { + cb.setPlaceholder('API key') + .setValue(this.plugin.settings.BoardgameGeekKey) + .onChange(data => { + this.plugin.settings.BoardgameGeekKey = data; + void this.plugin.saveSettings(); + }); }), ); diff --git a/src/settings/suggesters/FileSuggest.ts b/src/settings/suggesters/FileSuggest.ts index 6e911003..6bbdb968 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().includes(lowerCaseInputStr)); + .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); } renderSuggestion(value: TFile, el: HTMLElement): void { diff --git a/src/settings/suggesters/FolderSuggest.ts b/src/settings/suggesters/FolderSuggest.ts index 2ce3574e..eaee7153 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().includes(lowerCaseInputStr)); + .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); } renderSuggestion(value: TFolder, el: HTMLElement): void { diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts index da4b1a42..dcbaabb3 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 '../main'; -import { MediaDbBulkImportModal as MediaDbBulkImportModal } from '../modals/MediaDbBulkImportModal'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type MediaDbPlugin from 'src/main'; +import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; +import type { MediaTypeModel } from 'src/models/MediaTypeModel'; import { ModalResultCode } from './ModalHelper'; import { dateTimeToString, markdownTable } from './Utils'; diff --git a/src/utils/DateFormatter.ts b/src/utils/DateFormatter.ts index 31169e5b..0088af06 100644 --- a/src/utils/DateFormatter.ts +++ b/src/utils/DateFormatter.ts @@ -1,12 +1,6 @@ -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; +import { moment } from 'obsidian'; export class DateFormatter { - private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; - toFormat: string; locale: string; @@ -44,10 +38,18 @@ export class DateFormatter { return null; } - let date: Moment; + let date: moment.Moment; if (!dateFormat) { - date = this.parseWithoutFormat(dateString); + // 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)); + } } else { date = moment(dateString, dateFormat, locale); } @@ -56,20 +58,8 @@ 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, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode + const date = moment(dateString, true); // strict mode return date.isValid(); } } diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts index a8d15034..7efba29f 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 9e4f286c..9b25b34c 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -223,28 +223,10 @@ export function unCamelCase(str: string): string { ); } -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; -} +/* eslint-disable */ export function hasTemplaterPlugin(app: App): boolean { - const templater = getTemplaterPlugin(app); + const templater = (app as any).plugins.plugins['templater-obsidian']; return !!templater; } @@ -252,14 +234,17 @@ 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 = getTemplaterPlugin(app); - if (templater && !templater.settings?.trigger_on_file_creation && templater.templater) { + const templater = (app as any).plugins.plugins['templater-obsidian']; + if (templater && !templater?.settings.trigger_on_file_creation) { await templater.templater.overwrite_file_commands(file); } } +/* eslint-enable */ + export type ModelToData = { - [K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]?: T[K] | null; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + [K in keyof T as T[K] extends Function ? never : K]?: T[K] | null; }; // Checks if a given URL points to an existing image (status 200), or returns false for 404/other errors. @@ -287,8 +272,8 @@ export function isTruthy(value: T): value is Exclude { const obs_headers: Record = {}; - input.headers.forEach((value, key) => { - obs_headers[key] = value; + input.headers.forEach((header, value) => { + obs_headers[header] = value; }); const res = await requestUrl({ diff --git a/tsconfig.json b/tsconfig.json index 49b6fcbb..c6eb6724 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "baseUrl": ".", "module": "ESNext", "target": "ESNext", "allowJs": true, @@ -8,7 +9,7 @@ "strict": true, "strictNullChecks": true, "noImplicitReturns": true, - "moduleResolution": "bundler", + "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, "skipLibCheck": true, diff --git a/vite.config.ts b/vite.config.ts index 3a8f24b6..ffa6b85d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; -import { builtinModules } from 'node:module'; +import builtins from 'builtin-modules'; import { getBuildBanner } from './automation/build/buildBanner'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; @@ -72,7 +72,7 @@ export default defineConfig(({ mode }) => { '@lezer/common', '@lezer/highlight', '@lezer/lr', - ...builtinModules, + ...builtins, ], }, }, From fe5fdb975de1c284d4c9e0acca8eebabe2aa5231 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:05:45 +0200 Subject: [PATCH 10/35] migrate to secret storage and other improvements --- bun.lockb | Bin 211573 -> 234471 bytes package.json | 28 +- src/api/APIManager.ts | 4 +- src/api/apis/BoardGameGeekAPI.ts | 14 +- src/api/apis/ComicVineAPI.ts | 14 +- src/api/apis/GiantBombAPI.ts | 12 +- src/api/apis/MALAPI.ts | 2 +- src/api/apis/MALAPIManga.ts | 2 +- src/api/apis/MobyGamesAPI.ts | 10 +- src/api/apis/OMDbAPI.ts | 10 +- src/api/apis/OpenLibraryAPI.ts | 4 +- src/api/apis/TMDBMovieAPI.ts | 57 +++- src/api/apis/TMDBSeasonAPI.ts | 141 ++++---- src/api/apis/TMDBSeriesAPI.ts | 44 ++- src/main.ts | 58 ++-- src/modals/MediaDbBulkImportModal.ts | 4 +- src/modals/MediaDbPreviewModal.ts | 4 +- src/modals/MediaDbSearchModal.ts | 2 +- src/settings/PropertyMapper.ts | 79 ++--- .../PropertyMappingModelComponent.tsx | 2 +- src/settings/Settings.ts | 310 ++++++++++-------- src/settings/suggesters/FileSuggest.ts | 2 +- src/settings/suggesters/FolderSuggest.ts | 2 +- src/utils/BulkImportHelper.ts | 6 +- src/utils/DateFormatter.ts | 34 +- src/utils/ModalHelper.ts | 2 +- src/utils/Utils.ts | 35 +- tsconfig.json | 3 +- 28 files changed, 520 insertions(+), 365 deletions(-) diff --git a/bun.lockb b/bun.lockb index 419cad865cc97c68424cbbc91486c024ac6b6702..9f23f83d24f67c7519062e000a6a4b9ccb3531a1 100755 GIT binary patch delta 61357 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

5Z?&G8ww`T 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 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 07c20982..bddddb76 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 53d98207..da7e7adc 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 76e451b2..6d6d1ea4 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 8d0e34b6..3083e234 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 83d47322..2d39a4fa 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 a88eec94..a8972b3b 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 51b4f17a..38620807 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 645d15ba..f42f7918 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 936ab57e..4f4e1741 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 557eb4e6..09258031 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[] = []; - - for (const result of searchData.results) { - if (ret.length >= 20) break; - - // Fetch series details to get the total number of seasons - let totalSeasons = 0; - try { - const 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; + const topResults = searchData.results.slice(0, 20); + + 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, + }); + + 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', @@ -98,26 +127,25 @@ export class TMDBSeasonAPI extends APIModel { seasonTitle: result.name ?? result.original_name ?? '', seasonNumber: totalSeasons, image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', - }), - ); - } - - 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, }); @@ -160,8 +188,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.`); } @@ -171,20 +200,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, @@ -205,10 +234,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', }, @@ -240,28 +269,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 1c8a21fa..cb6ce28c 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 520516f2..eee47aae 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'; @@ -31,6 +30,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'; @@ -222,7 +222,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.'); @@ -377,28 +377,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 selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + })) ?? []; + if (selectResults.length < 1) { + return; + } - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - break; + const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { + return previewModalData.confirmed; + }); + if (!confirmed) { + return; } - await this.createMediaDbNotes(selectResults!); + await this.createMediaDbNotes(selectResults); } async createEntryWithIdSearchModal(): Promise { @@ -560,8 +554,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); @@ -578,8 +571,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, ''); @@ -713,6 +705,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 || [], @@ -723,6 +729,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 1aee8eeb..0763d32e 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 931556c1..1efd66a1 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 986cb8a8..800eacee 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 bf08d32b..b34f245d 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 d9a90173..c31bdbf9 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 f838410a..b1808884 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,28 +1,63 @@ import type { App } from 'obsidian'; -import { Notice, PluginSettingTab, 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 { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { fragWithHTML, unCamelCase } from '../utils/Utils'; -import type { PropertyMappingModelData } from './PropertyMapping'; -import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; -import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; -import { FileSuggest } from './suggesters/FileSuggest'; -import { FolderSuggest } from './suggesters/FolderSuggest'; +import { Notice, PluginSettingTab, SecretComponent, SettingGroup } from 'obsidian'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping'; +import PropertyMappingModelsComponent from 'packages/obsidian/src/settings/PropertyMappingModelsComponent'; +import { FileSuggest } from 'packages/obsidian/src/settings/suggesters/FileSuggest'; +import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderSuggest'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; + +function createDateFormatDescription(preview: string): DocumentFragment { + return createFragment(frag => { + const container = frag.createDiv(); + container.appendText('Your custom date format. Use '); + container.createEl('em', { text: "'YYYY-MM-DD'" }); + container.appendText(' for example.'); + container.createEl('br'); + container.appendText('For more syntax, refer to '); + container.createEl('a', { + href: 'https://momentjs.com/docs/#/displaying/format/', + text: 'format reference', + }); + container.appendText('.'); + container.createEl('br'); + container.appendText('Your current syntax looks like this: '); + container.createEl('em', { text: preview }); + }); +} + +function createPropertyMappingsDescription(): DocumentFragment { + return createFragment(frag => { + const container = frag.createDiv(); + container.createEl('p', { + text: 'Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.', + }); + container.createEl('p', { + text: 'You can choose to keep the original name, rename the property, or remove it entirely.', + }); + const paragraph = container.createEl('p'); + paragraph.createEl('strong', { + text: 'Remember to save your changes using the save button for each individual category.', + }); + }); +} // MARK: Settings export interface MediaDbPluginSettings { - OMDbKey: string; - TMDBKey: string; - MobyGamesKey: string; - GiantBombKey: string; + OMDbKeyId: string; + TMDBKeyId: string; + MobyGamesKeyId: string; + GiantBombKeyId: string; IGDBClientId: string; IGDBClientSecret: string; - RAWGAPIKey: string; - ComicVineKey: string; - BoardgameGeekKey: string; + RAWGAPIKeyId: string; + ComicVineKeyId: string; + BoardgameGeekKeyId: string; + sfwFilter: boolean; templates: boolean; customDateFormat: string; @@ -272,15 +307,16 @@ class MediaTypeMappedSettings { // MARK: Defaults const DEFAULT_SETTINGS: MediaDbPluginSettings = { - OMDbKey: '', - TMDBKey: '', - MobyGamesKey: '', - GiantBombKey: '', + OMDbKeyId: '', + TMDBKeyId: '', + MobyGamesKeyId: '', + GiantBombKeyId: '', IGDBClientId: '', IGDBClientSecret: '', - RAWGAPIKey: '', - ComicVineKey: '', - BoardgameGeekKey: '', + RAWGAPIKeyId: '', + ComicVineKeyId: '', + BoardgameGeekKeyId: '', + sfwFilter: true, templates: true, customDateFormat: 'L', @@ -371,7 +407,7 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings key, '', PropertyMappingOption.Default, - lockedPropertyMappings.contains(key), + lockedPropertyMappings.includes(key), false, // wikilink default ), ); @@ -388,14 +424,25 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; + private propertyMappingModelsComponent?: PropertyMappingModelsComponent; constructor(app: App, plugin: MediaDbPlugin) { super(app, plugin); this.plugin = plugin; } + override hide(): void { + this.propertyMappingModelsComponent?.unload(); + this.propertyMappingModelsComponent = undefined; + super.hide(); + } + display(): void { const { containerEl } = this; + if (this.propertyMappingModelsComponent) { + this.propertyMappingModelsComponent.unload(); + this.propertyMappingModelsComponent = undefined; + } containerEl.empty(); const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); @@ -433,22 +480,14 @@ export class MediaDbSettingTab extends PluginSettingTab { setting => void setting .setName('Date format') - .setDesc( - fragWithHTML( - "Your custom date format. Use 'YYYY-MM-DD' for example.
" + - "For more syntax, refer to
format reference.
" + - "Your current syntax looks like this: " + - this.plugin.dateFormatter.getPreview() + - '', - ), - ) + .setDesc(createDateFormatDescription(this.plugin.dateFormatter.getPreview())) .addText(cb => { cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) .onChange(data => { const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; this.plugin.settings.customDateFormat = newDateFormat; - const previewEl = document.getElementById('media-db-dateformat-preview'); + const previewEl = activeDocument.getElementById('media-db-dateformat-preview'); if (previewEl) { previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview } @@ -544,37 +583,31 @@ 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( setting => void setting - .setName('TMDB API Token') + .setName('TMDB API key') .setDesc('API Read Access Token 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( @@ -582,13 +615,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( @@ -596,13 +631,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( @@ -610,13 +647,15 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('IGDB Client ID') .setDesc('Client ID for IGDB API (Required for Twitch OAuth).') - .addText(cb => { - cb.setPlaceholder('Client ID') - .setValue(this.plugin.settings.IGDBClientId) - .onChange(data => { - this.plugin.settings.IGDBClientId = data; - void this.plugin.saveSettings(); - }); + .addComponent(el => { + const component = new SecretComponent(this.app, el); + + component.setValue(this.plugin.settings.IGDBClientId).onChange(data => { + this.plugin.settings.IGDBClientId = data; + void this.plugin.saveSettings(); + }); + + return component; }), ); apiKeyGroup.addSetting( @@ -624,13 +663,15 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('IGDB Client Secret') .setDesc('Client Secret for IGDB API.') - .addText(cb => { - cb.setPlaceholder('Client Secret') - .setValue(this.plugin.settings.IGDBClientSecret) - .onChange(data => { - this.plugin.settings.IGDBClientSecret = data; - void this.plugin.saveSettings(); - }); + .addComponent(el => { + const component = new SecretComponent(this.app, el); + + component.setValue(this.plugin.settings.IGDBClientSecret).onChange(data => { + this.plugin.settings.IGDBClientSecret = data; + void this.plugin.saveSettings(); + }); + + return component; }), ); apiKeyGroup.addSetting( @@ -638,13 +679,15 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('RAWG API Key') .setDesc('API key for "rawg.io".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.RAWGAPIKey) - .onChange(data => { - this.plugin.settings.RAWGAPIKey = data; - void this.plugin.saveSettings(); - }); + .addComponent(el => { + const component = new SecretComponent(this.app, el); + + component.setValue(this.plugin.settings.RAWGAPIKeyId).onChange(data => { + this.plugin.settings.RAWGAPIKeyId = data; + void this.plugin.saveSettings(); + }); + + return component; }), ); apiKeyGroup.addSetting( @@ -652,13 +695,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( @@ -666,13 +711,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; }), ); @@ -799,34 +846,23 @@ export class MediaDbSettingTab extends PluginSettingTab { const mappingGroup = new SettingGroup(containerEl); mappingGroup.setHeading('Property mappings'); mappingGroup.addSetting(setting => { - setting - .setName('Property mappings explanation') - .setDesc( - fragWithHTML( - '

Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.

' + - '

You can choose to keep the original name, rename the property, or remove it entirely.

' + - '

Remember to save your changes using the save button for each individual category.

', - ), - ); - - render( - () => - PropertyMappingModelsComponent({ - models: structuredClone(this.plugin.settings.propertyMappingModels), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }), - setting.descEl, - ); + setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription()); + const propertyMappingsEl = setting.descEl.createDiv(); + this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, { + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }); + this.propertyMappingModelsComponent.load(); }); } } -} +} \ No newline at end of file diff --git a/src/settings/suggesters/FileSuggest.ts b/src/settings/suggesters/FileSuggest.ts index 6bbdb968..6e911003 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 eaee7153..2ce3574e 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 dcbaabb3..da4b1a42 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 0088af06..31169e5b 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 7efba29f..a8d15034 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 9b25b34c..9e4f286c 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 c6eb6724..49b6fcbb 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, From d0118e5dbeb8113a1bfbc86c25789563bc3d8585 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:14:06 +0200 Subject: [PATCH 11/35] fix merge --- src/api/apis/IGDBAPI.ts | 100 +++++++++++++++++++++++++++++----------- src/api/apis/RAWGAPI.ts | 86 +++++++++++++++++++++++++--------- src/main.ts | 2 +- 3 files changed, 138 insertions(+), 50 deletions(-) diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 57a42f1e..14094d0b 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -5,16 +5,40 @@ import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; -interface IGDBCover { url: string; } -interface IGDBGenre { name: string; } -interface IGDBCompany { name: string; } -interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } +interface IGDBCover { + url: string; +} + +interface IGDBGenre { + name: string; +} + +interface IGDBCompany { + name: string; +} + +interface IGDBInvolvedCompany { + company: IGDBCompany; + developer: boolean; + publisher: boolean; +} + interface IGDBGame { - id: number; name: string; cover?: IGDBCover; first_release_date?: number; - summary?: string; total_rating?: number; url?: string; - genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; + id: number; + name: string; + cover?: IGDBCover; + first_release_date?: number; + summary?: string; + total_rating?: number; + url?: string; + genres?: IGDBGenre[]; + involved_companies?: IGDBInvolvedCompany[]; +} + +interface TwitchAuthResponse { + access_token: string; + expires_in: number; } -interface TwitchAuthResponse { access_token: string; expires_in: number; } export class IGDBAPI extends APIModel { plugin: MediaDbPlugin; @@ -35,58 +59,72 @@ export class IGDBAPI extends APIModel { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - if (!this.plugin.settings.IGDBClientId || !this.plugin.settings.IGDBClientSecret) { + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); + + if (!clientId || !clientSecret) { throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); } console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${this.plugin.settings.IGDBClientId}&client_secret=${this.plugin.settings.IGDBClientSecret}&grant_type=client_credentials`, + url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, method: 'POST', }); if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); const data = response.json as TwitchAuthResponse; this.accessToken = data.access_token; - this.tokenExpiry = currentTime + (data.expires_in * 1000) - 60000; + this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; return this.accessToken; } async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); const token = await this.getAuthToken(); const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; return data.map(result => { const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, year: year, - dataSource: this.apiName, id: result.id.toString(), image: image + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: year, + dataSource: this.apiName, + id: result.id.toString(), + image: image, }); }); } async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); const token = await this.getAuthToken(); const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); const result = data[0]; - + const developers: string[] = []; const publishers: string[] = []; result.involved_companies?.forEach(c => { @@ -97,15 +135,25 @@ export class IGDBAPI extends APIModel { const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, + type: MediaType.Game, + title: result.name, + englishTitle: result.name, year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', - dataSource: this.apiName, url: result.url, id: result.id.toString(), - developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], - onlineRating: result.total_rating, image: image, released: true, + dataSource: this.apiName, + url: result.url, + id: result.id.toString(), + developers: developers, + publishers: publishers, + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.total_rating, + image: image, + released: true, releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', userData: { played: false, personalRating: 0 }, }); } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; } -} \ No newline at end of file + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; + } +} diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index 1c79e5ae..ec0a53ea 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -6,11 +6,22 @@ import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; interface RAWGGame { - id: number; name: string; released?: string; background_image?: string; - name_original?: string; website?: string; slug?: string; metacritic?: number; - developers?: { name: string }[]; publishers?: { name: string }[]; genres?: { name: string }[]; + id: number; + name: string; + released?: string; + background_image?: string; + name_original?: string; + website?: string; + slug?: string; + metacritic?: number; + developers?: { name: string }[]; + publishers?: { name: string }[]; + genres?: { name: string }[]; +} + +interface RAWGSearchResponse { + results: RAWGGame[]; } -interface RAWGSearchResponse { results: RAWGGame[]; } export class RAWGAPI extends APIModel { plugin: MediaDbPlugin; @@ -26,40 +37,69 @@ export class RAWGAPI extends APIModel { } async searchByTitle(title: string): Promise { - if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${this.plugin.settings.RAWGAPIKey}&search=${encodeURIComponent(title)}&page_size=20`, + url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, method: 'GET', }); - if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + if (response.status !== 200) { + throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + } const data = response.json as RAWGSearchResponse; - return data.results.map(result => new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, id: result.id.toString(), image: result.background_image - })); + return data.results.map( + result => + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, + id: result.id.toString(), + image: result.background_image, + }), + ); } async getById(id: string): Promise { - if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${this.plugin.settings.RAWGAPIKey}`, + url: `${this.apiUrl}/games/${id}?key=${key}`, method: 'GET', }); - if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + if (response.status !== 200) { + throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + } const result = response.json as RAWGGame; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, + type: MediaType.Game, + title: result.name, + englishTitle: result.name_original ?? result.name, year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], - publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], - onlineRating: result.metacritic, image: result.background_image, - released: result.released != null, releaseDate: result.released, + dataSource: this.apiName, + url: result.website ?? `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), + developers: result.developers?.map(d => d.name) ?? [], + publishers: result.publishers?.map(p => p.name) ?? [], + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.metacritic, + image: result.background_image, + released: result.released != null, + releaseDate: result.released, userData: { played: false, personalRating: 0 }, }); } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; } -} \ No newline at end of file + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; + } +} diff --git a/src/main.ts b/src/main.ts index eee47aae..8a83e92c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,13 +6,13 @@ import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { ComicVineAPI } from './api/apis/ComicVineAPI'; import { GiantBombAPI } from './api/apis/GiantBombAPI'; import { IGDBAPI } from './api/apis/IGDBAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; +import { RAWGAPI } from './api/apis/RAWGAPI'; import { SteamAPI } from './api/apis/SteamAPI'; import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; From b41402b56393cf3d0a094388b3df0e8caa32527a Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:20:17 +0200 Subject: [PATCH 12/35] add unrelease changelog section --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f4a1c2..f1d32378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +# Unreleased + +- Added support for the `VNDB` API [#165](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/165) (thanks Senyksia) +- Added support for comic books through the `Comic Vine` API [#176](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/176) (thanks ltctceplrm) +- Fixed a typo issue in Steam search [#180](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/180) (thanks ltctceplrm) +- Added API toggle improvements and image download options [#183](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/183) (thanks ltctceplrm) +- Added a confirmation step before overwriting existing notes [#184](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/184) (thanks ltctceplrm) +- Improved Open Library integration and added plot/description support [#190](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/190) [#192](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/192) [#237](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/237) [#238](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/238) (thanks ltctceplrm) +- Added country, box office and age rating fields for movies and series [#196](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/196) (thanks ltctceplrm) +- Added release date support for MusicBrainz releases [#198](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/198) (thanks ZackBoe) +- Limited MusicBrainz cover art images to 500px [#199](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/199) (thanks ZackBoe) +- Added tracks, language, track count, and total duration support for music releases [#202](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/202) [#233](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/233) (thanks ltctceplrm) +- Cleaned up generated file names to remove illegal characters [#206](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/206) (thanks Spydarlee) +- Added batch/folder import by ID [#207](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/207) (thanks Spydarlee) +- Fixed double URI encoding when querying the Giant Bomb API by title [#209](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/209) (thanks Spydarlee) +- Improved property mappings with table view, wiki links option, and bug fixes [#195](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/195) [#231](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/231) (thanks ltctceplrm) +- Added TMDB season selection modal and improved season metadata handling [#220](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/220) (thanks ltctceplrm) +- Added board game API authorization and improved related error handling [#223](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/223) [#227](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/227) (thanks ltctceplrm) +- Swapped TMDB API key usage to TMDB API token [#246](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/246) (thanks ZackBoe) +- Updated comic/manga tags to vary based on subtype [#247](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/247) (thanks ltctceplrm) +- Re-implemented `IGDB` and `RAWG` providers with improved type safety and conflict resolution [#239](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/239) (thanks m24ih) +- Added support for Japanese titles in MyAnimeList [#253](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/253) (thanks ltctceplrm) +- Migrated plugin secrets to Obsidian secret storage +- Migrated settings UI from Svelte to Solid +- Various internal refactors, dependency updates, and bug fixes + # 0.8.0 - Fixed bugs when API keys for certain APIs were missing [#161](https://github.com/mProjectsCode/obsidian-media-db-plugin/pull/161) (thanks ltctceplrm) From e4cefe31d12214be6c18eab061dd454ebe1e2804 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:20:33 +0200 Subject: [PATCH 13/35] [auto] bump version to `0.8.0-canary.20260406T152025` --- manifest-beta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest-beta.json b/manifest-beta.json index e93a0224..dc561303 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260129T112027", + "version": "0.8.0-canary.20260406T152025", "minAppVersion": "1.5.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", From c6be7def92ae6728c6039c89e91f039d6d0daa9f Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:23:52 +0200 Subject: [PATCH 14/35] fix scripts --- automation/build/esbuild.config.ts | 6 +++--- automation/build/esbuild.dev.config.ts | 4 ++-- automation/fetchSchemas.ts | 2 +- automation/release.ts | 6 +++--- automation/tsconfig.json | 3 +-- automation/utils/versionUtils.ts | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/automation/build/esbuild.config.ts b/automation/build/esbuild.config.ts index 20d35b8f..f99de5cb 100644 --- a/automation/build/esbuild.config.ts +++ b/automation/build/esbuild.config.ts @@ -1,8 +1,8 @@ -import builtins from 'builtin-modules'; +import { builtinModules } from 'node:module'; import esbuild from 'esbuild'; import esbuildSvelte from 'esbuild-svelte'; import { sveltePreprocess } from 'svelte-preprocess'; -import { getBuildBanner } from 'build/buildBanner'; +import { getBuildBanner } from './buildBanner'; const banner = getBuildBanner('Release Build', version => version); @@ -26,7 +26,7 @@ const build = await esbuild.build({ '@lezer/common', '@lezer/highlight', '@lezer/lr', - ...builtins, + ...builtinModules, ], format: 'cjs', target: 'es2018', diff --git a/automation/build/esbuild.dev.config.ts b/automation/build/esbuild.dev.config.ts index 8d0e5157..463f3fee 100644 --- a/automation/build/esbuild.dev.config.ts +++ b/automation/build/esbuild.dev.config.ts @@ -2,8 +2,8 @@ import esbuild from 'esbuild'; import copy from 'esbuild-plugin-copy-watch'; import esbuildSvelte from 'esbuild-svelte'; import { sveltePreprocess } from 'svelte-preprocess'; -import manifest from '../../manifest.json' assert { type: 'json' }; -import { getBuildBanner } from 'build/buildBanner'; +import manifest from '../../manifest.json' with { type: 'json' }; +import { getBuildBanner } from './buildBanner'; const banner = getBuildBanner('Dev Build', _ => 'Dev Build'); diff --git a/automation/fetchSchemas.ts b/automation/fetchSchemas.ts index f3e2d482..c5c21024 100644 --- a/automation/fetchSchemas.ts +++ b/automation/fetchSchemas.ts @@ -1,4 +1,4 @@ -import { $ } from 'utils/shellUtils'; +import { $ } from './utils/shellUtils'; async function fetchSchema() { // https://docs.api.jikan.moe/ diff --git a/automation/release.ts b/automation/release.ts index 0ce8f0ef..9c601361 100644 --- a/automation/release.ts +++ b/automation/release.ts @@ -1,7 +1,7 @@ -import { UserError } from 'utils/utils'; -import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from 'utils/versionUtils'; +import { UserError } from './utils/utils'; +import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from './utils/versionUtils'; import config from './config.json'; -import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from 'utils/shellUtils'; +import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from './utils/shellUtils'; async function runPreconditions(): Promise { // run preconditions diff --git a/automation/tsconfig.json b/automation/tsconfig.json index 1fd339b5..45c24720 100644 --- a/automation/tsconfig.json +++ b/automation/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": ".", "module": "ESNext", "target": "ESNext", "allowJs": true, @@ -8,7 +7,7 @@ "strict": true, "strictNullChecks": true, "noImplicitReturns": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "importHelpers": true, "isolatedModules": true, "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"], diff --git a/automation/utils/versionUtils.ts b/automation/utils/versionUtils.ts index 62aad22c..8ad3fa29 100644 --- a/automation/utils/versionUtils.ts +++ b/automation/utils/versionUtils.ts @@ -2,7 +2,7 @@ import { Parser } from '@lemons_dev/parsinom/lib/Parser'; import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; import Moment from 'moment'; -import { UserError } from 'utils/utils'; +import { UserError } from './utils'; export class Version { major: number; From f55e4880a6ccfe51ef82fd1deebc4b8944c8433d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:24:04 +0200 Subject: [PATCH 15/35] [auto] bump version to `0.8.0-canary.20260406T152358` --- manifest-beta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest-beta.json b/manifest-beta.json index dc561303..c453e12d 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260406T152025", + "version": "0.8.0-canary.20260406T152358", "minAppVersion": "1.5.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", From 3721e144b29da3098205f0b0be5a2920e0b78905 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:27:08 +0200 Subject: [PATCH 16/35] fix scripts 2 --- automation/build/esbuild.config.ts | 57 -- automation/build/esbuild.dev.config.ts | 65 -- automation/tsconfig.json | 2 +- bun.lock | 810 +++++++++++++++++++++++++ bun.lockb | Bin 234471 -> 0 bytes vite.config.ts | 4 +- 6 files changed, 813 insertions(+), 125 deletions(-) delete mode 100644 automation/build/esbuild.config.ts delete mode 100644 automation/build/esbuild.dev.config.ts create mode 100644 bun.lock delete mode 100755 bun.lockb diff --git a/automation/build/esbuild.config.ts b/automation/build/esbuild.config.ts deleted file mode 100644 index f99de5cb..00000000 --- a/automation/build/esbuild.config.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { builtinModules } from 'node:module'; -import esbuild from 'esbuild'; -import esbuildSvelte from 'esbuild-svelte'; -import { sveltePreprocess } from 'svelte-preprocess'; -import { getBuildBanner } from './buildBanner'; - -const banner = getBuildBanner('Release Build', version => version); - -const build = await esbuild.build({ - banner: { - js: banner, - }, - entryPoints: ['src/main.ts'], - bundle: true, - external: [ - 'obsidian', - 'electron', - '@codemirror/autocomplete', - '@codemirror/collab', - '@codemirror/commands', - '@codemirror/language', - '@codemirror/lint', - '@codemirror/search', - '@codemirror/state', - '@codemirror/view', - '@lezer/common', - '@lezer/highlight', - '@lezer/lr', - ...builtinModules, - ], - format: 'cjs', - target: 'es2018', - logLevel: 'info', - sourcemap: false, - treeShaking: true, - outfile: 'main.js', - minify: true, - metafile: true, - define: { - MB_GLOBAL_CONFIG_DEV_BUILD: 'false', - }, - plugins: [ - esbuildSvelte({ - compilerOptions: { css: 'injected', dev: false }, - preprocess: sveltePreprocess(), - filterWarnings: warning => { - // we don't want warnings from node modules that we can do nothing about - return !warning.filename?.includes('node_modules'); - }, - }), - ], -}); - -const file = Bun.file('meta.txt'); -await Bun.write(file, JSON.stringify(build.metafile, null, '\t')); - -process.exit(0); diff --git a/automation/build/esbuild.dev.config.ts b/automation/build/esbuild.dev.config.ts deleted file mode 100644 index 463f3fee..00000000 --- a/automation/build/esbuild.dev.config.ts +++ /dev/null @@ -1,65 +0,0 @@ -import esbuild from 'esbuild'; -import copy from 'esbuild-plugin-copy-watch'; -import esbuildSvelte from 'esbuild-svelte'; -import { sveltePreprocess } from 'svelte-preprocess'; -import manifest from '../../manifest.json' with { type: 'json' }; -import { getBuildBanner } from './buildBanner'; - -const banner = getBuildBanner('Dev Build', _ => 'Dev Build'); - -const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ['src/main.ts'], - bundle: true, - external: [ - 'obsidian', - 'electron', - '@codemirror/autocomplete', - '@codemirror/collab', - '@codemirror/commands', - '@codemirror/language', - '@codemirror/lint', - '@codemirror/search', - '@codemirror/state', - '@codemirror/view', - '@lezer/common', - '@lezer/highlight', - '@lezer/lr', - ], - format: 'cjs', - target: 'es2018', - logLevel: 'info', - sourcemap: 'inline', - treeShaking: true, - outdir: `exampleVault/.obsidian/plugins/${manifest.id}/`, - outbase: 'src', - define: { - MB_GLOBAL_CONFIG_DEV_BUILD: 'true', - }, - plugins: [ - copy({ - paths: [ - { - from: './styles.css', - to: '', - }, - { - from: './manifest.json', - to: '', - }, - ], - }), - esbuildSvelte({ - compilerOptions: { css: 'injected', dev: true }, - preprocess: sveltePreprocess(), - filterWarnings: warning => { - // we don't want warnings from node modules that we can do nothing about - return !warning.filename?.includes('node_modules'); - }, - }), - ], -}); - -await context.watch(); diff --git a/automation/tsconfig.json b/automation/tsconfig.json index 45c24720..8e1333b3 100644 --- a/automation/tsconfig.json +++ b/automation/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "bundler", "importHelpers": true, "isolatedModules": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"], + "lib": ["DOM", "ES5", "ES6", "ES7", "Es2022"], "types": ["bun-types"], "allowSyntheticDefaultImports": true, "resolveJsonModule": true diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..2460bba7 --- /dev/null +++ b/bun.lock @@ -0,0 +1,810 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "obsidian-media-db-plugin", + "devDependencies": { + "@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.2.1", + "iso-639-2": "^3.0.2", + "obsidian": "latest", + "openapi-fetch": "^0.17.0", + "openapi-typescript": "^7.13.0", + "prettier": "^3.8.1", + "solid-js": "^1.9.12", + "string-argv": "^0.3.2", + "tslib": "^2.8.1", + "typescript": "^6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.5", + "vite-plugin-banner": "^0.8.1", + "vite-plugin-solid": "^2.11.12", + "vite-plugin-static-copy": "^4.0.1", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], + + "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], + + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.1.0", "", {}, "sha512-Jek2Drc1n6lRbr3QCB9JzK4rId5/U3rMmywiE/Quqmvq+f4unVqLeHe6XVTm6DeKn/T4Udgx/pC4Okt8FHX2sw=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], + + "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + + "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], + + "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], + + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.11", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + + "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], + + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001786", "", {}, "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iso-639-2": ["iso-639-2@3.0.2", "", {}, "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw=="], + + "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + + "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], + + "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="], + + "openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + + "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], + + "vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="], + + "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], + + "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], + + "vite-plugin-static-copy": ["vite-plugin-static-copy@4.0.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 9f23f83d24f67c7519062e000a6a4b9ccb3531a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 234471 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 Z1LI=!Li$hnu*#z*jZ6)*?*DiD{a^2CRWSen diff --git a/vite.config.ts b/vite.config.ts index ffa6b85d..3a8f24b6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; -import builtins from 'builtin-modules'; +import { builtinModules } from 'node:module'; import { getBuildBanner } from './automation/build/buildBanner'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; @@ -72,7 +72,7 @@ export default defineConfig(({ mode }) => { '@lezer/common', '@lezer/highlight', '@lezer/lr', - ...builtins, + ...builtinModules, ], }, }, From 260cd839ba8e65c0e61a2d759e2c0bd1d8672745 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Mon, 6 Apr 2026 17:27:24 +0200 Subject: [PATCH 17/35] [auto] bump version to `0.8.0-canary.20260406T152718` --- manifest-beta.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest-beta.json b/manifest-beta.json index c453e12d..de49a262 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260406T152358", + "version": "0.8.0-canary.20260406T152718", "minAppVersion": "1.5.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", From ec33974312b95d316172d202a99ae4194661ff05 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 6 May 2026 16:26:59 +0200 Subject: [PATCH 18/35] add obsidian eslint plugin, solve major issues --- .github/workflows/release.yml | 22 +- README.md | 34 +- __mocks__/obsidian.ts | 32 - automation/build/buildBanner.ts | 24 - automation/config.json | 5 - automation/fetchSchemas.ts | 20 - automation/release.ts | 171 ---- automation/stats.ts | 160 ---- automation/tsconfig.json | 19 - automation/utils/shellUtils.ts | 168 ---- automation/utils/utils.ts | 6 - automation/utils/versionUtils.ts | 108 --- bun.lock | 295 +++++-- eslint.config.mjs | 27 +- manifest.json | 2 +- package.json | 30 +- packages/obsidian/src/api/APIManager.ts | 113 +++ .../obsidian/src}/api/APIModel.ts | 12 +- .../obsidian/src/api/apis/BoardGameGeekAPI.ts | 217 +++++ .../obsidian/src/api/apis/ComicVineAPI.ts | 170 ++++ .../obsidian/src/api/apis/GiantBombAPI.ts | 169 ++++ packages/obsidian/src/api/apis/IGDBAPI.ts | 264 +++++++ packages/obsidian/src/api/apis/MALAPI.ts | 260 ++++++ .../obsidian/src}/api/apis/MALAPIManga.ts | 110 +-- .../obsidian/src/api/apis/MusicBrainzAPI.ts | 327 ++++++++ packages/obsidian/src/api/apis/OMDbAPI.ts | 388 +++++++++ .../obsidian/src/api/apis/OpenLibraryAPI.ts | 214 +++++ packages/obsidian/src/api/apis/RAWGAPI.ts | 161 ++++ .../obsidian/src}/api/apis/SteamAPI.ts | 144 ++-- .../obsidian/src/api/apis/TMDBMovieAPI.ts | 265 +++++++ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 440 +++++++++++ .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 255 ++++++ packages/obsidian/src/api/apis/VNDBAPI.ts | 334 ++++++++ .../obsidian/src/api/apis/WikipediaAPI.ts | 170 ++++ packages/obsidian/src/main.ts | 209 +++++ .../src}/modals/ConfirmOverwriteModal.ts | 0 .../src}/modals/MediaDbAdvancedSearchModal.ts | 16 +- .../src}/modals/MediaDbBulkImportModal.ts | 26 +- .../src}/modals/MediaDbIdSearchModal.ts | 14 +- .../src}/modals/MediaDbPreviewModal.ts | 13 +- .../src}/modals/MediaDbSearchModal.ts | 22 +- .../src}/modals/MediaDbSearchResultModal.ts | 13 +- .../src}/modals/MediaDbSeasonSelectModal.ts | 6 +- .../src}/modals/MediaItemComponent.ts | 0 .../obsidian/src}/modals/SelectModal.ts | 6 +- .../src}/modals/SelectModalElement.ts | 2 +- .../obsidian/src}/models/BoardGameModel.ts | 8 +- .../obsidian/src}/models/BookModel.ts | 8 +- .../obsidian/src}/models/ComicMangaModel.ts | 8 +- .../obsidian/src}/models/GameModel.ts | 8 +- .../obsidian/src}/models/MediaTypeModel.ts | 2 +- .../obsidian/src}/models/MovieModel.ts | 8 +- .../obsidian/src}/models/MusicReleaseModel.ts | 8 +- .../obsidian/src}/models/SeasonModel.ts | 8 +- .../obsidian/src}/models/SeriesModel.ts | 8 +- .../obsidian/src}/models/WikiModel.ts | 8 +- .../obsidian/src}/settings/Icon.tsx | 0 .../obsidian/src}/settings/PropertyMapper.ts | 11 +- .../obsidian/src}/settings/PropertyMapping.ts | 7 +- .../PropertyMappingModelComponent.tsx | 0 .../PropertyMappingModelsComponent.tsx | 0 .../obsidian/src}/settings/Settings.ts | 47 +- .../src}/settings/suggesters/FileSuggest.ts | 0 .../src}/settings/suggesters/FolderSuggest.ts | 0 {src => packages/obsidian/src}/styles.css | 4 + packages/obsidian/src/utils/AppError.ts | 24 + .../obsidian/src}/utils/BulkImportHelper.ts | 62 +- .../obsidian/src}/utils/DateFormatter.ts | 20 +- packages/obsidian/src/utils/ErrorReporter.ts | 23 + .../obsidian/src}/utils/IconList.ts | 0 .../utils/IllegalFilenameCharactersList.ts | 0 packages/obsidian/src/utils/Logger.ts | 38 + .../obsidian/src/utils/MediaDbEntryHelper.ts | 289 +++++++ .../obsidian/src/utils/MediaDbFileHelper.ts | 325 ++++++++ .../obsidian/src}/utils/MediaType.ts | 0 .../obsidian/src}/utils/MediaTypeManager.ts | 43 +- packages/obsidian/src/utils/ModalHelper.ts | 282 +++++++ {src => packages/obsidian/src}/utils/Utils.ts | 11 +- packages/obsidian/src/utils/result.ts | 52 ++ .../schemas/src}/GiantBomb.json | 0 .../schemas/src}/GiantBomb.ts | 0 .../schemas/src}/MALAPI.ts | 0 .../schemas/src}/OpenLibrary.json | 0 .../schemas/src}/OpenLibrary.ts | 0 .../schemas => packages/schemas/src}/TMDB.ts | 0 packages/schemas/src/fetchSchemas.sh | 15 + src/api/APIManager.ts | 82 -- src/api/apis/BoardGameGeekAPI.ts | 147 ---- src/api/apis/ComicVineAPI.ts | 117 --- src/api/apis/GiantBombAPI.ts | 167 ---- src/api/apis/IGDBAPI.ts | 159 ---- src/api/apis/MALAPI.ts | 229 ------ src/api/apis/MobyGamesAPI.ts | 121 --- src/api/apis/MusicBrainzAPI.ts | 261 ------ src/api/apis/OMDbAPI.ts | 289 ------- src/api/apis/OpenLibraryAPI.ts | 161 ---- src/api/apis/RAWGAPI.ts | 105 --- src/api/apis/TMDBMovieAPI.ts | 187 ----- src/api/apis/TMDBSeasonAPI.ts | 304 ------- src/api/apis/TMDBSeriesAPI.ts | 179 ----- src/api/apis/VNDBAPI.ts | 264 ------- src/api/apis/WikipediaAPI.ts | 112 --- src/main.ts | 743 ------------------ src/utils/ModalHelper.ts | 537 ------------- tsconfig.json | 9 +- vite.config.ts => vite.config.mts | 34 +- 106 files changed, 5683 insertions(+), 5314 deletions(-) delete mode 100644 __mocks__/obsidian.ts delete mode 100644 automation/build/buildBanner.ts delete mode 100644 automation/config.json delete mode 100644 automation/fetchSchemas.ts delete mode 100644 automation/release.ts delete mode 100644 automation/stats.ts delete mode 100644 automation/tsconfig.json delete mode 100644 automation/utils/shellUtils.ts delete mode 100644 automation/utils/utils.ts delete mode 100644 automation/utils/versionUtils.ts create mode 100644 packages/obsidian/src/api/APIManager.ts rename {src => packages/obsidian/src}/api/APIModel.ts (55%) create mode 100644 packages/obsidian/src/api/apis/BoardGameGeekAPI.ts create mode 100644 packages/obsidian/src/api/apis/ComicVineAPI.ts create mode 100644 packages/obsidian/src/api/apis/GiantBombAPI.ts create mode 100644 packages/obsidian/src/api/apis/IGDBAPI.ts create mode 100644 packages/obsidian/src/api/apis/MALAPI.ts rename {src => packages/obsidian/src}/api/apis/MALAPIManga.ts (52%) create mode 100644 packages/obsidian/src/api/apis/MusicBrainzAPI.ts create mode 100644 packages/obsidian/src/api/apis/OMDbAPI.ts create mode 100644 packages/obsidian/src/api/apis/OpenLibraryAPI.ts create mode 100644 packages/obsidian/src/api/apis/RAWGAPI.ts rename {src => packages/obsidian/src}/api/apis/SteamAPI.ts (50%) create mode 100644 packages/obsidian/src/api/apis/TMDBMovieAPI.ts create mode 100644 packages/obsidian/src/api/apis/TMDBSeasonAPI.ts create mode 100644 packages/obsidian/src/api/apis/TMDBSeriesAPI.ts create mode 100644 packages/obsidian/src/api/apis/VNDBAPI.ts create mode 100644 packages/obsidian/src/api/apis/WikipediaAPI.ts create mode 100644 packages/obsidian/src/main.ts rename {src => packages/obsidian/src}/modals/ConfirmOverwriteModal.ts (100%) rename {src => packages/obsidian/src}/modals/MediaDbAdvancedSearchModal.ts (86%) rename {src => packages/obsidian/src}/modals/MediaDbBulkImportModal.ts (79%) rename {src => packages/obsidian/src}/modals/MediaDbIdSearchModal.ts (82%) rename {src => packages/obsidian/src}/modals/MediaDbPreviewModal.ts (80%) rename {src => packages/obsidian/src}/modals/MediaDbSearchModal.ts (82%) rename {src => packages/obsidian/src}/modals/MediaDbSearchResultModal.ts (90%) rename {src => packages/obsidian/src}/modals/MediaDbSeasonSelectModal.ts (88%) rename {src => packages/obsidian/src}/modals/MediaItemComponent.ts (100%) rename {src => packages/obsidian/src}/modals/SelectModal.ts (95%) rename {src => packages/obsidian/src}/modals/SelectModalElement.ts (96%) rename {src => packages/obsidian/src}/models/BoardGameModel.ts (78%) rename {src => packages/obsidian/src}/models/BookModel.ts (77%) rename {src => packages/obsidian/src}/models/ComicMangaModel.ts (82%) rename {src => packages/obsidian/src}/models/GameModel.ts (76%) rename {src => packages/obsidian/src}/models/MediaTypeModel.ts (92%) rename {src => packages/obsidian/src}/models/MovieModel.ts (82%) rename {src => packages/obsidian/src}/models/MusicReleaseModel.ts (81%) rename {src => packages/obsidian/src}/models/SeasonModel.ts (82%) rename {src => packages/obsidian/src}/models/SeriesModel.ts (82%) rename {src => packages/obsidian/src}/models/WikiModel.ts (75%) rename {src => packages/obsidian/src}/settings/Icon.tsx (100%) rename {src => packages/obsidian/src}/settings/PropertyMapper.ts (87%) rename {src => packages/obsidian/src}/settings/PropertyMapping.ts (96%) rename {src => packages/obsidian/src}/settings/PropertyMappingModelComponent.tsx (100%) rename {src => packages/obsidian/src}/settings/PropertyMappingModelsComponent.tsx (100%) rename {src => packages/obsidian/src}/settings/Settings.ts (95%) rename {src => packages/obsidian/src}/settings/suggesters/FileSuggest.ts (100%) rename {src => packages/obsidian/src}/settings/suggesters/FolderSuggest.ts (100%) rename {src => packages/obsidian/src}/styles.css (99%) create mode 100644 packages/obsidian/src/utils/AppError.ts rename {src => packages/obsidian/src}/utils/BulkImportHelper.ts (67%) rename {src => packages/obsidian/src}/utils/DateFormatter.ts (80%) create mode 100644 packages/obsidian/src/utils/ErrorReporter.ts rename {src => packages/obsidian/src}/utils/IconList.ts (100%) rename {src => packages/obsidian/src}/utils/IllegalFilenameCharactersList.ts (100%) create mode 100644 packages/obsidian/src/utils/Logger.ts create mode 100644 packages/obsidian/src/utils/MediaDbEntryHelper.ts create mode 100644 packages/obsidian/src/utils/MediaDbFileHelper.ts rename {src => packages/obsidian/src}/utils/MediaType.ts (100%) rename {src => packages/obsidian/src}/utils/MediaTypeManager.ts (82%) create mode 100644 packages/obsidian/src/utils/ModalHelper.ts rename {src => packages/obsidian/src}/utils/Utils.ts (96%) create mode 100644 packages/obsidian/src/utils/result.ts rename {src/api/schemas => packages/schemas/src}/GiantBomb.json (100%) rename {src/api/schemas => packages/schemas/src}/GiantBomb.ts (100%) rename {src/api/schemas => packages/schemas/src}/MALAPI.ts (100%) rename {src/api/schemas => packages/schemas/src}/OpenLibrary.json (100%) rename {src/api/schemas => packages/schemas/src}/OpenLibrary.ts (100%) rename {src/api/schemas => packages/schemas/src}/TMDB.ts (100%) create mode 100644 packages/schemas/src/fetchSchemas.sh delete mode 100644 src/api/APIManager.ts delete mode 100644 src/api/apis/BoardGameGeekAPI.ts delete mode 100644 src/api/apis/ComicVineAPI.ts delete mode 100644 src/api/apis/GiantBombAPI.ts delete mode 100644 src/api/apis/IGDBAPI.ts delete mode 100644 src/api/apis/MALAPI.ts delete mode 100644 src/api/apis/MobyGamesAPI.ts delete mode 100644 src/api/apis/MusicBrainzAPI.ts delete mode 100644 src/api/apis/OMDbAPI.ts delete mode 100644 src/api/apis/OpenLibraryAPI.ts delete mode 100644 src/api/apis/RAWGAPI.ts delete mode 100644 src/api/apis/TMDBMovieAPI.ts delete mode 100644 src/api/apis/TMDBSeasonAPI.ts delete mode 100644 src/api/apis/TMDBSeriesAPI.ts delete mode 100644 src/api/apis/VNDBAPI.ts delete mode 100644 src/api/apis/WikipediaAPI.ts delete mode 100644 src/main.ts delete mode 100644 src/utils/ModalHelper.ts rename vite.config.ts => vite.config.mts (70%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2594994d..e7202e78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,9 +13,14 @@ jobs: build: runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine prerelease status id: status @@ -27,23 +32,32 @@ jobs: fi - name: Install Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Build id: build run: | - bun install + bun install --frozen-lockfile bun run build mkdir ${{ env.PLUGIN_NAME }} cp dist/main.js dist/manifest.json dist/styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip ${{ env.PLUGIN_NAME }} ls + - name: Attest build provenance + uses: actions/attest@v4 + with: + subject-path: | + dist/main.js + dist/manifest.json + dist/styles.css + ${{ env.PLUGIN_NAME }}-${{ github.ref_name }}.zip + - name: Release id: release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: prerelease: ${{ steps.status.outputs.prerelease }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 7cd31f37..5a3e0902 100644 --- a/README.md +++ b/README.md @@ -116,20 +116,18 @@ Now you select the result you want, and the plugin will cast its magic, creating ### Currently supported APIs: -| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | -| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes | -| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movies, series, and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No | -| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (**not** `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes | -| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No | -| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | -| [Steam](https://store.steampowered.com/) | The Steam API offers information on all Steam games. | games | No | 10000 per day | No | -| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID, so there shouldn't be a rate limit. | No | -| [Moby Games](https://www.mobygames.com) | The Moby Games API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.mobygames.com/user/register/). NOTE: As of September 2024 the API key is no longer free so consider using Giant Bomb or steam instead | API requests are limited to 360 per hour (one every ten seconds). In addition, requests should be made no more frequently than one per second. | No | -| [Giant Bomb](https://www.giantbomb.com) | The Giant Bomb API offers metadata for games for all platforms | games | Yes, by making an account [here](https://www.giantbomb.com/login-signup/) | API requests are limited to 200 requests per resource, per hour. In addition, they implement velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | -| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | -| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | -| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No | +| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | +| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs, manga, manwha, novels | No | 60 per minute and 3 per second | Yes | +| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movies, series, and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No | +| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Read Access Token` (**not** `API Key`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes | +| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No | +| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No | +| [Steam](https://store.steampowered.com/) | The Steam API offers information on all Steam games. | games | No | 10000 per day | No | +| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID, so there shouldn't be a rate limit. | No | +| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No | +| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes | +| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No | #### Notes @@ -137,6 +135,8 @@ Now you select the result you want, and the plugin will cast its magic, creating - sometimes the api is very slow; this is normal - you need to use the title the anime has on [My Anime List](https://myanimelist.net), which is in most cases the Japanese title - e.g. instead of "Demon Slayer" you have to search "Kimetsu no Yaiba" +- Support for the [Moby Games](https://www.mobygames.com) API has been removed from the plugins as the API is no longer free to use. +- Support for the [Giant Bomb](https://www.giantbomb.com) API has been removed from the plugin temporarily as their API is [currently non functional](https://giantbomb.com/api). #### Search by ID @@ -174,12 +174,6 @@ Now you select the result you want, and the plugin will cast its magic, creating - This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) ` - For a specific edition of "Fantastic Mr. Fox" the "/books/" URL looks like this `https://openlibrary.org/books/OL3567303M/` so the ID is `/books/OL3567303M` - This URL is located in the editions section` -- [Moby Games](https://www.mobygames.com) - - you can find this ID in the URL - - e.g. for "Bioshock 2" the URL looks like this `https://www.mobygames.com/game/45089/bioshock-2/` so the ID is `45089` -- [Giant Bomb](https://www.giantbomb.com) - - you can find this ID in the URL - - e.g. for "Dota 2" the URL looks like this `https://www.giantbomb.com/dota-2/3030-32887/` so the ID is `3030-32887` - [Comic Vine](https://www.comicvine.gamespot.com) - you can find this ID in the URL - e.g. for "Boule & Bill" the URL looks like this `https://comicvine.gamespot.com/boule-bill/4050-70187/` so the ID is `4050-70187` diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts deleted file mode 100644 index 5ae02058..00000000 --- a/__mocks__/obsidian.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RequestUrlParam, RequestUrlResponse } from 'obsidian'; - -export function requestUrl(request: RequestUrlParam): Promise { - return fetch(request.url, { - method: request.method, - headers: request.headers, - body: request.body, - }).then(async response => { - if (response.status >= 400 && request.throw) { - throw new Error(`Request failed, ${response.status}`); - } - - // Turn response headers into Record object - const headers: Record = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const arraybuffer = await response.arrayBuffer(); - const text = arraybuffer ? new TextDecoder().decode(arraybuffer) : ''; - const json = text ? JSON.parse(text) : {}; - - let response_body: RequestUrlResponse = { - status: response.status, - headers: headers, - arrayBuffer: arraybuffer, - json: json, - text: text, - }; - return response_body; - }); -} diff --git a/automation/build/buildBanner.ts b/automation/build/buildBanner.ts deleted file mode 100644 index 849aec39..00000000 --- a/automation/build/buildBanner.ts +++ /dev/null @@ -1,24 +0,0 @@ -import manifest from '../../manifest.json' with { type: 'json' }; - -export function getBuildBanner(buildType: string, getVersion: (version: string) => string) { - return `/* -------------------------------------------- -${manifest.name} - ${buildType} -------------------------------------------- -By: ${manifest.author} (${manifest.authorUrl}) -Time: ${new Date().toUTCString()} -Version: ${getVersion(manifest.version)} -------------------------------------------- -THIS IS A GENERATED/BUNDLED FILE -if you want to view the source, please visit the github repository of this plugin -------------------------------------------- -Copyright (c) ${new Date().getFullYear()} ${manifest.author} -------------------------------------------- -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . -*/ -`; -} diff --git a/automation/config.json b/automation/config.json deleted file mode 100644 index c9a89476..00000000 --- a/automation/config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devBranch": "master", - "releaseBranch": "release", - "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin" -} diff --git a/automation/fetchSchemas.ts b/automation/fetchSchemas.ts deleted file mode 100644 index c5c21024..00000000 --- a/automation/fetchSchemas.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { $ } from './utils/shellUtils'; - -async function fetchSchema() { - // https://docs.api.jikan.moe/ - await $('bun openapi-typescript https://raw.githubusercontent.com/jikan-me/jikan-rest/master/storage/api-docs/api-docs.json -o ./src/api/schemas/MALAPI.ts'); - - // https://www.giantbomb.com/forums/api-developers-3017/giant-bomb-openapi-specification-1901269/ - await $('bun openapi-typescript ./src/api/schemas/GiantBomb.json -o ./src/api/schemas/GiantBomb.ts'); - - // https://www.omdbapi.com/swagger.json - // await $('bun openapi-typescript ./src/api/schemas/OMDb.json -o ./src/api/schemas/OMDb.ts'); - - // https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml - await $('bun openapi-typescript ./src/api/schemas/OpenLibrary.json -o ./src/api/schemas/OpenLibrary.ts'); - - // https://developer.themoviedb.org/openapi - await $('bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./src/api/schemas/TMDB.ts'); -} - -await fetchSchema(); diff --git a/automation/release.ts b/automation/release.ts deleted file mode 100644 index 9c601361..00000000 --- a/automation/release.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { UserError } from './utils/utils'; -import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from './utils/versionUtils'; -import config from './config.json'; -import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from './utils/shellUtils'; - -async function runPreconditions(): Promise { - // run preconditions - await $seq( - [`bun run format`, `bun run test`], - (cmd: string) => { - throw new UserError(`precondition "${cmd}" failed`); - }, - () => {}, - undefined, - Verboseness.VERBOSE, - ); - - // add changed files - await $seq( - [`git add .`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - // check if there were any changes - let changesToCommit = false; - await $seq( - [`git diff --quiet`, `git diff --cached --quiet`], - () => { - changesToCommit = true; - }, - () => {}, - undefined, - Verboseness.QUITET, - ); - - // if there were any changes, commit them - if (changesToCommit) { - await $seq( - [`git commit -m "[auto] run release preconditions"`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - } -} - -async function run() { - console.log('looking for untracked changes ...'); - - // check for any uncommited files and exit if there are any - await $seq( - [`git add .`, `git diff --quiet`, `git diff --cached --quiet`, `git checkout ${config.devBranch}`], - () => { - throw new UserError('there are still untracked changes'); - }, - () => {}, - undefined, - Verboseness.QUITET, - ); - - console.log('\nrunning preconditions ...\n'); - - await runPreconditions(); - - console.log('\nbumping versions ...\n'); - - const manifestFile = Bun.file('./manifest.json'); - const manifest = await manifestFile.json(); - - const versionString: string = manifest.version; - const currentVersion: Version = parseVersion(versionString); - const currentVersionString = stringifyVersion(currentVersion); - - const versionIncrementOptions = getIncrementOptions(currentVersion); - - const selectedIndex = await $choice( - `Current version "${currentVersionString}". Select new version`, - versionIncrementOptions.map(x => stringifyVersion(x)), - ); - const newVersion = versionIncrementOptions[selectedIndex]; - const newVersionString = stringifyVersion(newVersion); - - console.log(''); - - await $confirm(`Version will be updated "${currentVersionString}" -> "${newVersionString}". Are you sure`, () => { - throw new UserError('user canceled script'); - }); - - if (!(newVersion instanceof CanaryVersion)) { - manifest.version = newVersionString; - } - - await Bun.write(manifestFile, JSON.stringify(manifest, null, '\t')); - - const betaManifest = structuredClone(manifest); - betaManifest.version = newVersionString; - - const betaManifestFile = Bun.file('./manifest-beta.json'); - await Bun.write(betaManifestFile, JSON.stringify(betaManifest, null, '\t')); - - if (!(newVersion instanceof CanaryVersion)) { - const versionsFile = Bun.file('./versions.json'); - const versionsJson = await versionsFile.json(); - - versionsJson[newVersionString] = manifest.minAppVersion; - - await Bun.write(versionsFile, JSON.stringify(versionsJson, null, '\t')); - - const packageFile = Bun.file('./package.json'); - const packageJson = await packageFile.json(); - - packageJson.version = newVersionString; - - await Bun.write(packageFile, JSON.stringify(packageJson, null, '\t')); - } - - await $seq( - [`bun run format`, `git add .`, `git commit -m "[auto] bump version to \`${newVersionString}\`"`], - () => { - throw new UserError('failed to add preconditions changes to git'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - console.log('\ncreating release tag ...\n'); - - await $seq( - [ - `git checkout ${config.releaseBranch}`, - `git merge ${config.devBranch} --commit -m "[auto] merge \`${newVersionString}\` release commit"`, - `git push origin ${config.releaseBranch}`, - `git tag -a ${newVersionString} -m "release version ${newVersionString}"`, - `git push origin ${newVersionString}`, - `git checkout ${config.devBranch}`, - `git merge ${config.releaseBranch}`, - `git push origin ${config.devBranch}`, - ], - () => { - throw new UserError('failed to merge or create tag'); - }, - () => {}, - undefined, - Verboseness.NORMAL, - ); - - console.log(''); - - console.log(`${CMD_FMT.BgGreen}done${CMD_FMT.Reset}`); - console.log(`${config.github}`); - console.log(`${config.github}/releases/tag/${newVersionString}`); -} - -try { - await run(); -} catch (e) { - if (e instanceof UserError) { - console.error(e.message); - } else { - console.error(e); - } -} diff --git a/automation/stats.ts b/automation/stats.ts deleted file mode 100644 index 2a34c036..00000000 --- a/automation/stats.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as fs from 'fs'; - -interface Stat { - fileType: string; - count: number; - lines: number; -} - -abstract class StatsBase { - parent: StatsBase | undefined; - path: string; - name: string; - stats: Stat[]; - - constructor(parent: StatsBase | undefined, path: string, name: string, stats: Stat[]) { - this.parent = parent; - - this.path = path; - this.name = name; - this.stats = stats; - } - - abstract addChild(child: StatsBase): void; - - abstract mergeStats(stats: Stat[]): void; - - abstract print(depth: number, lastChildArr: boolean[]): void; - - abstract sort(): void; - - getPrefix(depth: number, lastChildArr: boolean[]): string { - let prefix = ''; - for (let i = 0; i < depth; i++) { - prefix += lastChildArr[i] ? ' ' : '│ '; - } - - if (lastChildArr.at(-1)) { - prefix += '└─ '; - } else { - prefix += '├─ '; - } - - return prefix; - } -} - -class FolderStats extends StatsBase { - children: StatsBase[]; - - constructor(parent: StatsBase | undefined, path: string, name: string) { - super(parent, path, name, []); - - this.children = []; - } - - addChild(child: StatsBase) { - this.children.push(child); - this.mergeStats(child.stats); - } - - mergeStats(stats: Stat[]): void { - // console.log(this, stats); - for (const stat of stats) { - const existingStat = this.stats.find(s => s.fileType === stat.fileType); - if (existingStat) { - existingStat.count += stat.count; - existingStat.lines += stat.lines; - } else { - this.stats.push(structuredClone(stat)); - } - } - - this.parent?.mergeStats(stats); - } - - print(depth: number, lastChildArr: boolean[]): void { - console.log( - `${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats.reduce((acc, s) => acc + s.count, 0)} files | ${this.stats.reduce((acc, s) => acc + s.lines, 0)} lines`, - ); - for (let i = 0; i < this.children.length; i++) { - const child = this.children[i]; - child.print(depth + 1, [...lastChildArr, i === this.children.length - 1]); - } - } - - sort(): void { - this.children.sort((a, b) => { - if (a instanceof FolderStats && b instanceof FileStats) { - return 1; - } else if (a instanceof FileStats && b instanceof FolderStats) { - return -1; - } else { - return a.name.localeCompare(b.name); - } - }); - this.children.forEach(c => c.sort()); - } -} - -class FileStats extends StatsBase { - constructor(parent: StatsBase, path: string, name: string, stats: Stat[]) { - super(parent, path, name, stats); - } - - addChild(_child: StatsBase): void { - throw new Error('Cannot add child to file'); - } - - mergeStats(_stats: Stat[]): void { - throw new Error('Cannot merge stats to file'); - } - - print(depth: number, lastChildArr: boolean[]): void { - console.log(`${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats[0].lines} lines`); - } - - sort(): void {} -} - -function collectStats() { - const root = new FolderStats(undefined, './src', 'src'); - const ignore = ['node_modules', 'extraTypes', 'bun.lockb']; - - const todo: FolderStats[] = [root]; - - while (todo.length > 0) { - const current = todo.pop()!; - const children = fs.readdirSync(current.path, { withFileTypes: true }); - - for (const child of children) { - if (ignore.includes(child.name)) { - continue; - } - - if (child.isDirectory()) { - const folder = new FolderStats(current, `${current.path}/${child.name}`, child.name); - current.addChild(folder); - - todo.push(folder); - } else { - const content = fs.readFileSync(`${current.path}/${child.name}`, 'utf-8'); - - const file = new FileStats(current, `${current.path}/${child.name}`, child.name, [ - { - fileType: child.name.split('.').splice(1).join('.'), - count: 1, - lines: content.split('\n').length, - }, - ]); - current.addChild(file); - } - } - } - - root.sort(); - - root.print(0, [true]); -} - -collectStats(); diff --git a/automation/tsconfig.json b/automation/tsconfig.json deleted file mode 100644 index 8e1333b3..00000000 --- a/automation/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "module": "ESNext", - "target": "ESNext", - "allowJs": true, - "noImplicitAny": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "moduleResolution": "bundler", - "importHelpers": true, - "isolatedModules": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "Es2022"], - "types": ["bun-types"], - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true - }, - "include": ["**/*.ts"] -} diff --git a/automation/utils/shellUtils.ts b/automation/utils/shellUtils.ts deleted file mode 100644 index d1c58e08..00000000 --- a/automation/utils/shellUtils.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Subprocess } from 'bun'; -import stringArgv from 'string-argv'; - -export enum Verboseness { - QUITET, - NORMAL, - VERBOSE, -} - -function exec(c: string, cwd?: string): Subprocess<'ignore', 'pipe', 'inherit'> { - return Bun.spawn(stringArgv(c), { cwd: cwd }); -} - -export async function $(cmd: string, cwd?: string | undefined, verboseness: Verboseness = Verboseness.NORMAL): Promise<{ stdout: string; stderr: string; exit: number }> { - if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { - if (cwd !== undefined) { - console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} in ${cwd} - ${cmd}\n`); - } else { - console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} - ${cmd}\n`); - } - } - - const proc = exec(cmd, cwd); - const stdout = await new Response(proc.stdout).text(); - const stderr = await new Response(proc.stderr).text(); - - if (verboseness === Verboseness.VERBOSE) { - if (stdout !== '') { - console.log( - stdout - .split('\n') - .map(x => `${CMD_FMT.FgGray}>${CMD_FMT.Reset} ${x}\n`) - .join(''), - ); - } - - if (stderr !== '') { - console.log( - stderr - .split('\n') - .map(x => `${CMD_FMT.FgRed}>${CMD_FMT.Reset} ${x}\n`) - .join(''), - ); - } - } - - const exit = await proc.exited; - - if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { - if (exit === 0) { - console.log(`${CMD_FMT.FgGreen}success${CMD_FMT.Reset} - ${cmd}\n`); - } else { - console.log(`${CMD_FMT.FgRed}fail${CMD_FMT.Reset} - ${cmd} - code ${exit}\n`); - } - } - - return { - stdout, - stderr, - exit, - }; -} - -export async function $seq( - cmds: string[], - onError: (cmd: string, index: number) => void, - onSuccess: () => void, - cwd?: string | undefined, - verboseness: Verboseness = Verboseness.NORMAL, -): Promise { - const results = []; - for (let i = 0; i < cmds.length; i += 1) { - const cmd = cmds[i]; - const result = await $(cmd, cwd, verboseness); - - if (result.exit !== 0) { - onError(cmd, i); - return; - } - - results.push(result); - } - onSuccess(); -} - -export async function $input(message: string): Promise { - console.write(`${message} `); - const stdin = Bun.stdin.stream(); - const reader = stdin.getReader(); - const chunk = await reader.read(); - reader.releaseLock(); - const text = Buffer.from(chunk.value ?? '').toString(); - return text.trim(); -} - -export async function $choice(message: string, options: string[]): Promise { - console.log(`${message} `); - - let optionNumbers = new Map(); - - for (let i = 0; i < options.length; i++) { - const option = options[i]; - console.log(`[${i}] ${option}`); - - optionNumbers.set(i.toString(), i); - } - - let ret: undefined | number = undefined; - - while (ret === undefined) { - const selectedStr = await $input(`Select [${[...optionNumbers.keys()].join('/')}]:`); - - ret = optionNumbers.get(selectedStr); - - if (ret === undefined) { - console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); - } - } - - return ret; -} - -export async function $confirm(message: string, onReject: () => void): Promise { - while (true) { - const answer = await $input(`${message} [Y/N]?`); - - if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { - return; - } - - if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { - onReject(); - return; - } - - console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); - } -} - -export const CMD_FMT = { - Reset: '\x1b[0m', - Bright: '\x1b[1m', - Dim: '\x1b[2m', - Underscore: '\x1b[4m', - Blink: '\x1b[5m', - Reverse: '\x1b[7m', - Hidden: '\x1b[8m', - - FgBlack: '\x1b[30m', - FgRed: '\x1b[31m', - FgGreen: '\x1b[32m', - FgYellow: '\x1b[33m', - FgBlue: '\x1b[34m', - FgMagenta: '\x1b[35m', - FgCyan: '\x1b[36m', - FgWhite: '\x1b[37m', - FgGray: '\x1b[90m', - - BgBlack: '\x1b[40m', - BgRed: '\x1b[41m', - BgGreen: '\x1b[42m', - BgYellow: '\x1b[43m', - BgBlue: '\x1b[44m', - BgMagenta: '\x1b[45m', - BgCyan: '\x1b[46m', - BgWhite: '\x1b[47m', - BgGray: '\x1b[100m', -}; diff --git a/automation/utils/utils.ts b/automation/utils/utils.ts deleted file mode 100644 index f5d72eec..00000000 --- a/automation/utils/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class UserError extends Error {} - -export interface ProjectConfig { - corePackages: string[]; - packages: string[]; -} diff --git a/automation/utils/versionUtils.ts b/automation/utils/versionUtils.ts deleted file mode 100644 index 8ad3fa29..00000000 --- a/automation/utils/versionUtils.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Parser } from '@lemons_dev/parsinom/lib/Parser'; -import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; -import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; -import Moment from 'moment'; -import { UserError } from './utils'; - -export class Version { - major: number; - minor: number; - patch: number; - - constructor(major: number, minor: number, patch: number) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - toString(): string { - return `${this.major}.${this.minor}.${this.patch}`; - } -} - -export class CanaryVersion extends Version { - canary: string; - - constructor(major: number, minor: number, patch: number, canary: string) { - super(major, minor, patch); - this.canary = canary; - } - - toString(): string { - return `${super.toString()}-canary.${this.canary}`; - } -} - -const numberParser: Parser = P_UTILS.digits() - .map(x => Number.parseInt(x)) - .chain(x => { - if (Number.isNaN(x)) { - return P.fail('a number'); - } else { - return P.succeed(x); - } - }); - -const canaryParser: Parser = P.sequenceMap( - (_, c1, c2, c3) => { - return c1 + c2 + c3; - }, - P.string('-canary.'), - P_UTILS.digit() - .repeat(8, 8) - .map(x => x.join('')), - P.string('T'), - P_UTILS.digit() - .repeat(6, 6) - .map(x => x.join('')), -); - -export const versionParser: Parser = P.or( - P.sequenceMap( - (major, _1, minor, _2, patch) => { - return new Version(major, minor, patch); - }, - numberParser, - P.string('.'), - numberParser, - P.string('.'), - numberParser, - P_UTILS.eof(), - ), - P.sequenceMap( - (major, _1, minor, _2, patch, canary) => { - return new CanaryVersion(major, minor, patch, canary); - }, - numberParser, - P.string('.'), - numberParser, - P.string('.'), - numberParser, - canaryParser, - P_UTILS.eof(), - ), -); - -export function parseVersion(str: string): Version { - const parserRes = versionParser.tryParse(str); - if (parserRes.success) { - return parserRes.value; - } else { - throw new UserError(`failed to parse manifest version "${str}"`); - } -} - -export function stringifyVersion(version: Version): string { - return version.toString(); -} - -export function getIncrementOptions(version: Version): [Version, Version, Version, CanaryVersion] { - const moment = Moment(); - const canary = moment.utcOffset(0).format('YYYYMMDDTHHmmss'); - return [ - new Version(version.major + 1, 0, 0), - new Version(version.major, version.minor + 1, 0), - new Version(version.major, version.minor, version.patch + 1), - new CanaryVersion(version.major, version.minor, version.patch, canary), - ]; -} diff --git a/bun.lock b/bun.lock index 2460bba7..9f91cfa9 100644 --- a/bun.lock +++ b/bun.lock @@ -5,33 +5,34 @@ "": { "name": "obsidian-media-db-plugin", "devDependencies": { - "@happy-dom/global-registrator": "^20.8.9", - "@lemons_dev/parsinom": "^0.1.0", - "@types/bun": "^1.3.11", + "@happy-dom/global-registrator": "^20.9.0", + "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", + "@types/bun": "^1.3.13", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-obsidianmd": "^0.2.9", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.1", + "prettier": "^3.8.3", "solid-js": "^1.9.12", - "string-argv": "^0.3.2", "tslib": "^2.8.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", + "vite": "^8.0.10", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1", + "vite-plugin-static-copy": "^4.1.0", }, }, }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], @@ -55,7 +56,7 @@ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -69,9 +70,9 @@ "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], - "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -89,18 +90,24 @@ "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/json": ["@eslint/json@0.14.0", "", { "dependencies": { "@eslint/core": "^0.17.0", "@eslint/plugin-kit": "^0.4.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.9.0" } }, "sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="], + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -113,55 +120,61 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.1.0", "", {}, "sha512-Jek2Drc1n6lRbr3QCB9JzK4rId5/U3rMmywiE/Quqmvq+f4unVqLeHe6XVTm6DeKn/T4Udgx/pC4Okt8FHX2sw=="], + "@lemons_dev/lemons-obsidian-plugin-automation": ["@lemons_dev/lemons-obsidian-plugin-automation@0.1.3", "", { "dependencies": { "@lemons_dev/parsinom": "^0.2.1", "moment": "^2.30.1", "string-argv": "^0.3.2" }, "peerDependencies": { "vite": "^8.0.9" }, "bin": { "lemons-automation": "bin/lemons-automation.js" } }, "sha512-Nv1eONy1Q9citS+nsMMasgL9ZfhKb57uinJmHRF+F1H+9HSiRkB5wlx9J/S1ou9cJL8IAhFvv72KGzE9HTIWtg=="], + + "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.2.1", "", {}, "sha512-ew+G3gm5aWJBnIhrysxI7k09FCAtAoj48wbTzr16NoX9ndhB8UnhmqjTxYVv46DsG88HQo+DTWPIFQW5qTAwwg=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="], + "@microsoft/eslint-plugin-sdl": ["@microsoft/eslint-plugin-sdl@1.1.0", "", { "dependencies": { "eslint-plugin-n": "17.10.3", "eslint-plugin-react": "7.37.3", "eslint-plugin-security": "1.4.0" }, "peerDependencies": { "eslint": "^9" } }, "sha512-dxdNHOemLnBhfY3eByrujX9KyLigcNtW8sU+axzWv5nLGcsSBeKW2YYyTpfPo1hV8YPOmIGnfA4fZHyKVtWqBQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="], "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.11", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-V09ayfnb5GyysmvARbt+voFZAjGcf7hSYxOYxSkCc4fbH/DTfq5YWoec8cflvmHHqyIFbqvmGKmYFzqhr9zxDg=="], + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -171,17 +184,19 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -189,25 +204,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -215,7 +230,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -229,12 +244,16 @@ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -247,19 +266,19 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -267,7 +286,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001786", "", {}, "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -311,16 +330,22 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="], + "electron-to-chromium": ["electron-to-chromium@1.5.351", "", {}, "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA=="], + + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -335,14 +360,34 @@ "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="], + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "eslint-plugin-depend": ["eslint-plugin-depend@1.3.1", "", { "dependencies": { "empathic": "^2.0.0", "module-replacements": "^2.8.0", "semver": "^7.6.3" } }, "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw=="], + + "eslint-plugin-es-x": ["eslint-plugin-es-x@7.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", "@eslint-community/regexpp": "^4.11.0", "eslint-compat-utils": "^0.5.1" }, "peerDependencies": { "eslint": ">=8" } }, "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ=="], + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + "eslint-plugin-json-schema-validator": ["eslint-plugin-json-schema-validator@5.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", "ajv": "^8.0.0", "debug": "^4.3.1", "eslint-compat-utils": "^0.5.0", "json-schema-migrate": "^2.0.0", "jsonc-eslint-parser": "^2.0.0", "minimatch": "^8.0.0", "synckit": "^0.9.0", "toml-eslint-parser": "^0.9.0", "tunnel-agent": "^0.6.0", "yaml-eslint-parser": "^1.0.0" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-ZmVyxRIjm58oqe2kTuy90PpmZPrrKvOjRPXKzq8WCgRgAkidCgm5X8domL2KSfadZ3QFAmifMgGTcVNhZ5ez2g=="], + + "eslint-plugin-n": ["eslint-plugin-n@17.10.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "enhanced-resolve": "^5.17.0", "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "globals": "^15.8.0", "ignore": "^5.2.4", "minimatch": "^9.0.5", "semver": "^7.5.3" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw=="], + + "eslint-plugin-no-relative-import-paths": ["eslint-plugin-no-relative-import-paths@1.6.1", "", {}, "sha512-YZNeOnsOrJcwhFw0X29MXjIzu2P/f5X2BZDPWw1R3VUYBRFxNIh77lyoL/XrMU9ewZNQPcEvAgL/cBOT1P330A=="], + + "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], + + "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="], + "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], + "eslint-plugin-react": ["eslint-plugin-react@7.37.3", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA=="], + + "eslint-plugin-security": ["eslint-plugin-security@2.1.1", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w=="], + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -363,6 +408,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -395,6 +442,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -403,7 +452,9 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -417,7 +468,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -445,7 +496,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], @@ -493,6 +544,8 @@ "iso-639-2": ["iso-639-2@3.0.2", "", {}, "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw=="], + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -503,12 +556,18 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-migrate": ["json-schema-migrate@2.0.0", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "jsonc-eslint-parser": ["jsonc-eslint-parser@2.4.2", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", "semver": "^7.3.5" } }, "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -541,6 +600,8 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -551,20 +612,24 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "module-replacements": ["module-replacements@2.11.0", "", {}, "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA=="], + + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -617,18 +682,24 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -637,19 +708,27 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -663,7 +742,7 @@ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], @@ -679,6 +758,10 @@ "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], @@ -695,16 +778,24 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "synckit": ["synckit@0.9.3", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toml-eslint-parser": ["toml-eslint-parser@0.9.3", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0" } }, "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -717,13 +808,13 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], + "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -731,13 +822,13 @@ "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], - "vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="], + "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "vite-plugin-static-copy": ["vite-plugin-static-copy@4.0.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA=="], + "vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.0", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], @@ -761,8 +852,12 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], + "yaml-eslint-parser": ["yaml-eslint-parser@1.3.2", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0", "yaml": "^2.0.0" } }, "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -771,6 +866,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@microsoft/eslint-plugin-sdl/eslint-plugin-security": ["eslint-plugin-security@1.4.0", "", { "dependencies": { "safe-regex": "^1.1.0" } }, "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA=="], + "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], @@ -791,20 +888,66 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "eslint-compat-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-depend/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-json-schema-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "eslint-plugin-json-schema-validator/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "eslint-plugin-n/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + + "eslint-plugin-obsidianmd/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + + "json-schema-migrate/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "jsonc-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], + "toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "yaml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@microsoft/eslint-plugin-sdl/eslint-plugin-security/safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="], + + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "eslint-plugin-json-schema-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/eslint.config.mjs b/eslint.config.mjs index c634b74c..5f855726 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,17 +1,25 @@ // @ts-check import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import only_warn from 'eslint-plugin-only-warn'; -import * as plugin_import from 'eslint-plugin-import'; +import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths'; +import obsidianmd from 'eslint-plugin-obsidianmd'; -export default tseslint.config( +export default defineConfig( { - ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css', 'src/api/schemas/'], + ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '*.css'], }, { - files: ['src/**/*.ts'], - extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked], + files: ['packages/obsidian/src/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ...obsidianmd.configs.recommended, + ], languageOptions: { parser: tseslint.parser, parserOptions: { @@ -21,7 +29,8 @@ export default tseslint.config( plugins: { // @ts-ignore 'only-warn': only_warn, - import: plugin_import, + 'no-relative-import-paths': no_relative_import_paths, + obsidianmd: obsidianmd, }, rules: { '@typescript-eslint/no-explicit-any': ['warn'], @@ -44,11 +53,15 @@ export default tseslint.config( '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], '@typescript-eslint/restrict-template-expressions': 'off', + 'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }], + '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/explicit-function-return-type': ['warn'], - '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-deprecated': 'off', + + 'obsidianmd/ui/sentence-case': 'off', }, }, ); diff --git a/manifest.json b/manifest.json index a8d101ff..5e91e7f7 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "obsidian-media-db-plugin", "name": "Media DB", "version": "0.8.0", - "minAppVersion": "1.5.0", + "minAppVersion": "1.12.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev", diff --git a/package.json b/package.json index 815c67a1..5661e62e 100644 --- a/package.json +++ b/package.json @@ -6,41 +6,41 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "bun run tsc && vite build --mode production", - "tsc": "tsc -noEmit -skipLibCheck", + "typecheck": "tsc -noEmit -skipLibCheck", "test": "bun test", "test:log": "LOG_TESTS=true bun test", "format": "prettier --write .", "format:check": "prettier --check .", - "lint": "eslint --max-warnings=0 --no-warn-ignored src/**", - "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**", - "check": "bun run format:check && bun run tsc && bun run lint", - "check:fix": "bun run format && bun run tsc && bun run lint:fix", - "release": "bun run automation/release.ts", - "stats": "bun run automation/stats.ts" + "lint": "eslint --max-warnings=0 --no-warn-ignored packages/obsidian/src/**", + "lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored packages/obsidian/src/**", + "check": "bun run format:check && bun run typecheck && bun run lint", + "check:fix": "bun run format && bun run typecheck && bun run lint:fix", + "release": "lemons-automation release" }, "keywords": [], "author": "Moritz Jung", "license": "GPL-3.0", "devDependencies": { - "@happy-dom/global-registrator": "^20.8.9", - "@lemons_dev/parsinom": "^0.1.0", - "@types/bun": "^1.3.11", + "@happy-dom/global-registrator": "^20.9.0", + "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", + "@types/bun": "^1.3.13", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-obsidianmd": "^0.2.9", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", - "prettier": "^3.8.1", + "prettier": "^3.8.3", "solid-js": "^1.9.12", - "string-argv": "^0.3.2", "tslib": "^2.8.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.0", - "vite": "^8.0.5", + "vite": "^8.0.10", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", - "vite-plugin-static-copy": "^4.0.1" + "vite-plugin-static-copy": "^4.1.0" } } diff --git a/packages/obsidian/src/api/APIManager.ts b/packages/obsidian/src/api/APIManager.ts new file mode 100644 index 00000000..de9ba412 --- /dev/null +++ b/packages/obsidian/src/api/APIManager.ts @@ -0,0 +1,113 @@ +import type { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; + +export interface ApiQueryOk { + items: MediaTypeModel[]; + warnings: AppError[]; +} + +export class APIManager { + apis: APIModel[]; + + constructor() { + this.apis = []; + } + + /** + * Queries the basic info for one query string and multiple APIs. + * + * @param query + * @param apisToQuery + */ + async query(query: string, apisToQuery: string[]): Promise> { + Logger.debug(`MDB | api manager queried with "${query}"`); + + const apis = this.apis.filter(api => apisToQuery.includes(api.apiName)); + const results = await Promise.all(apis.map(api => api.searchByTitle(query))); + + const items: MediaTypeModel[] = []; + const warnings: AppError[] = []; + for (const result of results) { + if (result.ok) { + items.push(...result.value); + } else { + warnings.push(result.error); + } + } + + if (items.length === 0 && warnings.length > 0) { + // If all APIs failed, surface an error (using the first as representative) + return err( + toAppError(warnings[0], { + kind: AppErrorKind.Api, + message: 'Failed to query APIs', + userMessage: 'Failed to query APIs', + context: { query, apisToQuery }, + }), + ); + } + + for (const warning of warnings) { + Logger.warn(warning); + } + + return ok({ items, warnings }); + } + + /** + * Queries detailed information for a MediaTypeModel. + * + * @param item + */ + async queryDetailedInfo(item: MediaTypeModel): Promise> { + return await this.queryDetailedInfoById(item.id, item.dataSource); + } + + /** + * Queries detailed info for an id from an API. + * + * @param id + * @param apiName + */ + async queryDetailedInfoById(id: string, apiName: string): Promise> { + for (const api of this.apis) { + if (api.apiName === apiName) { + const result = await api.getById(id); + + if (!result.ok) { + Logger.warn(result.error); + } + + return result.ok ? ok(result.value) : err(result.error); + } + } + + return err( + toAppError(new Error(`API not found: ${apiName}`), { + kind: AppErrorKind.Validation, + message: `API not found: ${apiName}`, + userMessage: `API not found: ${apiName}`, + context: { apiName, id }, + }), + ); + } + + getApiByName(name: string): APIModel | undefined { + for (const api of this.apis) { + if (api.apiName === name) { + return api; + } + } + + return undefined; + } + + registerAPI(api: APIModel): void { + this.apis.push(api); + } +} diff --git a/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts similarity index 55% rename from src/api/APIModel.ts rename to packages/obsidian/src/api/APIModel.ts index 0919db90..7527e99e 100644 --- a/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,6 +1,8 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from '../utils/MediaType'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; export abstract class APIModel { apiName!: string; @@ -14,9 +16,9 @@ export abstract class APIModel { * * @param title the title to query for */ - abstract searchByTitle(title: string): Promise; + abstract searchByTitle(title: string): Promise>; - abstract getById(id: string): Promise; + abstract getById(id: string): Promise>; abstract getDisabledMediaTypes(): MediaType[]; diff --git a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts new file mode 100644 index 00000000..889b779f --- /dev/null +++ b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts @@ -0,0 +1,217 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +// sadly no open api schema available + +export class BoardGameGeekAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'BoardGameGeekAPI'; + this.apiDescription = 'A free API for BoardGameGeek things.'; + this.apiUrl = 'https://boardgamegeek.com/xmlapi/'; + this.types = [MediaType.BoardGame]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${key}`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + + // console.debug(response); + + const ret: MediaTypeModel[] = []; + + for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { + const id = boardgame.attributes.getNamedItem('objectid')?.value; + const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined; + const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; + + ret.push( + new BoardGameModel({ + dataSource: this.apiName, + id, + title, + englishTitle: title, + year, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${key}`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + // console.debug(response); + + const boardgame = response.querySelector('boardgame'); + if (!boardgame) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received invalid data from ${this.apiName}.`, + userMessage: `Received invalid data from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + const title = boardgame.querySelector('name[primary=true]')?.textContent; + const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; + const image = boardgame.querySelector('image')?.textContent ?? undefined; + const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); + const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')) + .map(n => n.textContent) + .filter(n => n !== null); + const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); + const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); + const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); + const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; + const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')) + .map(n => n.textContent) + .filter(n => n !== null); + + return ok( + new BoardGameModel({ + title: title ?? undefined, + englishTitle: title ?? undefined, + year: year === '0' ? '' : year, + dataSource: this.apiName, + url: `https://boardgamegeek.com/boardgame/${id}`, + id: id, + + genres: genres, + onlineRating: onlineRating, + complexityRating: complexityRating, + minPlayers: minPlayers, + maxPlayers: maxPlayers, + playtime: playtime, + publishers: publishers, + image: image, + + released: true, + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/ComicVineAPI.ts b/packages/obsidian/src/api/apis/ComicVineAPI.ts new file mode 100644 index 00000000..0babc397 --- /dev/null +++ b/packages/obsidian/src/api/apis/ComicVineAPI.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ + +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +// sadly no open api schema available + +export class ComicVineAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'ComicVineAPI'; + this.apiDescription = 'A free API for comic books.'; + this.apiUrl = 'https://comicvine.gamespot.com/api'; + this.types = [MediaType.ComicManga]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const searchUrl = `${this.apiUrl}/search/?api_key=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + // console.debug(fetchData); + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.results) { + ret.push( + new ComicMangaModel({ + title: result.name, + englishTitle: result.name, + year: result.start_year, + dataSource: this.apiName, + id: `4050-${result.id}`, + publishers: result.publisher?.name, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${key}&format=json`; + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + Logger.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const data = await fetchData.json; + const result = data.results; + + const authors = result.people as + | { + name: string; + }[] + | undefined; + + return ok( + new ComicMangaModel({ + type: MediaType.ComicManga, + title: result.name, + englishTitle: result.name, + alternateTitles: result.aliases, + plot: result.deck, + year: result.start_year, + dataSource: this.apiName, + url: result.site_detail_url, + id: `4050-${result.id}`, + + authors: authors?.map(x => x.name), + chapters: result.count_of_issues, + image: result.image?.original_url, + + released: true, + publishers: result.publisher?.name, + publishedFrom: result.start_year, + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.ComicVineAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/GiantBombAPI.ts b/packages/obsidian/src/api/apis/GiantBombAPI.ts new file mode 100644 index 00000000..2346fbe1 --- /dev/null +++ b/packages/obsidian/src/api/apis/GiantBombAPI.ts @@ -0,0 +1,169 @@ +// Temporarily removed until they get their API working again. + +// import createClient from 'openapi-fetch'; +// 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'; + +// export class GiantBombAPI extends APIModel { +// plugin: MediaDbPlugin; +// apiDateFormat: string = 'YYYY-MM-DD'; + +// constructor(plugin: MediaDbPlugin) { +// super(); + +// this.plugin = plugin; +// this.apiName = 'GiantBombAPI'; +// this.apiDescription = 'A free API for games.'; +// this.apiUrl = 'https://www.giantbomb.com/api'; +// this.types = [MediaType.Game]; +// } + +// 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 (!key) { +// throw Error(`MDB | API key for ${this.apiName} missing.`); +// } + +// const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); +// const response = await client.GET('/games', { +// params: { +// query: { +// api_key: key, +// filter: `name:${title}`, +// format: 'json', +// limit: 20, +// }, +// }, +// fetch: obsidianFetch, +// }); + +// if (response.response.status === 401) { +// throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); +// } +// if (response.response.status === 429) { +// throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); +// } +// if (response.response.status !== 200) { +// throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); +// } + +// const data = response.data?.results; + +// const ret: MediaTypeModel[] = []; +// for (const result of data ?? []) { +// const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; + +// ret.push( +// new GameModel({ +// title: result.name, +// englishTitle: result.name, +// year: year, +// dataSource: this.apiName, +// id: result.guid?.toString(), +// }), +// ); +// } + +// return ret; +// } + +// 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 (!key) { +// throw Error(`MDB | API key for ${this.apiName} missing.`); +// } + +// const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); +// const response = await client.GET('/game/{guid}', { +// params: { +// path: { +// guid: id, +// }, +// query: { +// api_key: key, +// format: 'json', +// }, +// }, +// fetch: obsidianFetch, +// }); + +// if (response.response.status === 401) { +// throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); +// } +// if (response.response.status === 429) { +// throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); +// } +// if (response.response.status !== 200) { +// throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); +// } + +// const result = response.data?.results; + +// if (!result) { +// throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); +// } + +// console.log(result); + +// // sadly the only OpenAPI definition I could find doesn't have the right types +// const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; +// const developers = result.developers as +// | { +// name: string; +// }[] +// | undefined; +// const publishers = result.publishers as +// | { +// name: string; +// }[] +// | undefined; +// const genres = result.genres as +// | { +// name: string; +// }[] +// | undefined; +// const image = result.image as +// | { +// small_url: string; +// medium_url: string; +// super_url: string; +// } +// | undefined; + +// return new GameModel({ +// type: MediaType.Game, +// title: result.name, +// englishTitle: result.name, +// year: year, +// dataSource: this.apiName, +// url: result.site_detail_url, +// id: result.guid?.toString(), +// developers: developers?.map(x => x.name), +// publishers: publishers?.map(x => x.name), +// genres: genres?.map(x => x.name), +// onlineRating: 0, +// image: image?.super_url, + +// released: true, +// releaseDate: result.original_release_date, + +// userData: { +// played: false, + +// personalRating: 0, +// }, +// }); +// } +// getDisabledMediaTypes(): MediaType[] { +// return this.plugin.settings.GiantBombAPI_disabledMediaTypes; +// } +// } diff --git a/packages/obsidian/src/api/apis/IGDBAPI.ts b/packages/obsidian/src/api/apis/IGDBAPI.ts new file mode 100644 index 00000000..acfbf61d --- /dev/null +++ b/packages/obsidian/src/api/apis/IGDBAPI.ts @@ -0,0 +1,264 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface IGDBCover { + url: string; +} + +interface IGDBGenre { + name: string; +} + +interface IGDBCompany { + name: string; +} + +interface IGDBInvolvedCompany { + company: IGDBCompany; + developer: boolean; + publisher: boolean; +} + +interface IGDBGame { + id: number; + name: string; + cover?: IGDBCover; + first_release_date?: number; + summary?: string; + total_rating?: number; + url?: string; + genres?: IGDBGenre[]; + involved_companies?: IGDBInvolvedCompany[]; +} + +interface TwitchAuthResponse { + access_token: string; + expires_in: number; +} + +export class IGDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + private accessToken: string = ''; + private tokenExpiry: number = 0; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'IGDBAPI'; + this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; + this.apiUrl = 'https://api.igdb.com/v4'; + this.types = [MediaType.Game]; + } + + private async getAuthToken(): Promise> { + const currentTime = Date.now(); + if (this.accessToken && currentTime < this.tokenExpiry) return ok(this.accessToken); + + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); + + if (!clientId || !clientSecret) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID or Client Secret for ${this.apiName} missing.`, + userMessage: `Client ID or Client Secret for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + Logger.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); + const responseResult = await fromPromise( + requestUrl({ + url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, + method: 'POST', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying Twitch auth for ${this.apiName}`, + userMessage: `Network error querying Twitch auth for ${this.apiName}`, + context: { apiName: this.apiName }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Auth failed for ${this.apiName}. Check Credentials.`, + userMessage: `Auth failed for ${this.apiName}. Check Credentials.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as TwitchAuthResponse; + this.accessToken = data.access_token; + this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; + return ok(this.accessToken); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID for ${this.apiName} missing.`, + userMessage: `Client ID for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + const tokenResult = await this.getAuthToken(); + if (!tokenResult.ok) { + return err(tokenResult.error); + } + const token = tokenResult.value; + const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + body: queryBody, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as IGDBGame[]; + return ok( + data.map(result => { + const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + return new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: year, + dataSource: this.apiName, + id: result.id.toString(), + image: image, + }); + }), + ); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); + if (!clientId) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Client ID for ${this.apiName} missing.`, + userMessage: `Client ID for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + const tokenResult = await this.getAuthToken(); + if (!tokenResult.ok) { + return err(tokenResult.error); + } + const token = tokenResult.value; + const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, + body: queryBody, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const data = response.json as IGDBGame[]; + if (!data || data.length === 0) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No result found for ID ${id}`, + userMessage: `No result found for ID ${id}`, + context: { apiName: this.apiName, id }, + }); + } + const result = data[0]; + + const developers: string[] = []; + const publishers: string[] = []; + result.involved_companies?.forEach(c => { + if (c.developer) developers.push(c.company.name); + if (c.publisher) publishers.push(c.company.name); + }); + const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', + dataSource: this.apiName, + url: result.url, + id: result.id.toString(), + developers: developers, + publishers: publishers, + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.total_rating, + image: image, + released: true, + releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', + userData: { played: false, personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; + } +} diff --git a/packages/obsidian/src/api/apis/MALAPI.ts b/packages/obsidian/src/api/apis/MALAPI.ts new file mode 100644 index 00000000..11d1c02f --- /dev/null +++ b/packages/obsidian/src/api/apis/MALAPI.ts @@ -0,0 +1,260 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import { isTruthy, obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/MALAPI'; + +export class MALAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MALAPI'; + this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; + this.apiUrl = 'https://jikan.moe/'; + this.types = [MediaType.Movie, MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('special', 'special'); + this.typeMappings.set('tv', 'series'); + this.typeMappings.set('ova', 'ova'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime', { + params: { + query: { + q: title, + limit: 20, + sfw: this.plugin.settings.sfwFilter ? true : false, + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data?.data; + + const ret: MediaTypeModel[] = []; + + for (const result of data ?? []) { + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString() ?? ''; + const id = result.mal_id?.toString(); + + if (type === undefined) { + ret.push( + new MovieModel({ + subType: '', + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + if (type === 'movie' || type === 'special') { + ret.push( + new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } else if (type === 'series' || type === 'ova') { + ret.push( + new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime/{id}/full', { + params: { + path: { + id: id as unknown as number, // This is fine + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data?.data; + + if (result === undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id} in ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString(); + const new_id = result.mal_id?.toString(); + + if (type === undefined) { + return ok( + new MovieModel({ + subType: undefined, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: result.duration, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + if (type === 'movie' || type === 'special') { + return ok( + new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: result.duration, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'series' || type === 'ova') { + return ok( + new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + episodes: result.episodes, + duration: result.duration, + onlineRating: result.score, + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), + airing: result.airing, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + return err({ + kind: AppErrorKind.Unexpected, + message: `MDB | Unknown media type for id ${id}`, + userMessage: `Unknown media type for id ${id}`, + context: { apiName: this.apiName, id }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MALAPI_disabledMediaTypes; + } +} diff --git a/src/api/apis/MALAPIManga.ts b/packages/obsidian/src/api/apis/MALAPIManga.ts similarity index 52% rename from src/api/apis/MALAPIManga.ts rename to packages/obsidian/src/api/apis/MALAPIManga.ts index 2d39a4fa..424449b3 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/packages/obsidian/src/api/apis/MALAPIManga.ts @@ -1,11 +1,16 @@ import createClient from 'openapi-fetch'; -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'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import { isTruthy, obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/MALAPI'; export class MALAPIManga extends APIModel { plugin: MediaDbPlugin; @@ -30,8 +35,8 @@ export class MALAPIManga extends APIModel { this.typeMappings.set('novel', 'novel'); } - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -47,7 +52,12 @@ export class MALAPIManga extends APIModel { }); if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); } const data = response.data?.data; @@ -93,11 +103,11 @@ export class MALAPIManga extends APIModel { ); } - return ret; + return ok(ret); } - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -111,13 +121,23 @@ export class MALAPIManga extends APIModel { }); if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); } const result = response.data?.data; if (!result) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id} in ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); } const resType = result.type?.toLowerCase(); @@ -125,35 +145,37 @@ export class MALAPIManga extends APIModel { const year = result.published?.prop?.from?.year?.toString() ?? ''; const new_id = result.mal_id?.toString(); - return new ComicMangaModel({ - subType: type, - title: result.title, - plot: result.synopsis ?? undefined, - englishTitle: result.title_english ?? result.title, - alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - genres: result.genres?.map(x => x.name).filter(isTruthy), - authors: result.authors?.map(x => x.name).filter(isTruthy), - chapters: result.chapters, - volumes: result.volumes, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), - publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); + return ok( + new ComicMangaModel({ + subType: type, + title: result.title, + plot: result.synopsis ?? undefined, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + genres: result.genres?.map(x => x.name).filter(isTruthy), + authors: result.authors?.map(x => x.name).filter(isTruthy), + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), + publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); } getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.MALAPIManga_disabledMediaTypes; diff --git a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts new file mode 100644 index 00000000..a61d7d0b --- /dev/null +++ b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts @@ -0,0 +1,327 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from 'packages/obsidian/src/utils/Utils'; + +// sadly no open api schema available + +interface Tag { + name: string; + count: number; +} +interface Genre { + name: string; + count: number; + id: string; + disambiguation: string; +} +interface Release { + id: string; + 'status-id': string; + title: string; + status: string; +} + +interface ArtistCredit { + name: string; + artist: { + tags: Tag[]; + type: string; + id: string; + name: string; + 'short-name': string; + country: string; + }; +} + +interface SearchResponse { + id: string; + 'type-id': string; + score: number; + 'primary-type-id': string; + 'artists-credit-id': string; + count: number; + title: string; + 'first-release-date': string; + 'primary-type': string; + 'artist-credit': ArtistCredit[]; + releases: Release[]; + tags: Tag[]; +} + +interface IdResponse { + id: string; + tags: Tag[]; + 'primary-type-id': string; + 'artist-credit': ArtistCredit[]; + title: string; + genres: Genre[]; + 'first-release-date': string; + releases: Release[]; + 'primary-type': string; + rating: { + value: number; + 'votes-count': number; + }; +} + +interface MediaResponse { + media: { + 'track-count': number; + tracks: { + 'artist-credit': ArtistCredit[]; + length: number | null; + number: string; + position: number; + title: string; + recording: { + length: number; + title: string; + }; + }[]; + }[]; + 'text-representation': { + language: string; + script: string; + }; +} + +export class MusicBrainzAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MusicBrainz API'; + this.apiDescription = 'Free API for music albums.'; + this.apiUrl = 'https://musicbrainz.org/'; + this.types = [MediaType.MusicRelease]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; + + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + // console.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const data = (await fetchData.json) as { + 'release-groups': SearchResponse[]; + }; + // console.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data['release-groups']) { + ret.push( + new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: new Date(result['first-release-date']).getFullYear().toString(), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + subType: result['primary-type'], + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + // Fetch release group + const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; + const groupResponseResult = await fromPromise( + requestUrl({ + url: groupUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!groupResponseResult.ok) { + return err(groupResponseResult.error); + } + const groupResponse = groupResponseResult.value; + + if (groupResponse.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${groupResponse.status} from ${this.apiName}.`, + userMessage: `Received status code ${groupResponse.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: groupResponse.status, id }, + }); + } + + const result = (await groupResponse.json) as IdResponse; + + // Get ID of the first release + const firstRelease = result.releases?.[0]; + if (!firstRelease) { + return err({ + kind: AppErrorKind.Api, + message: 'MDB | No releases found in release group.', + userMessage: 'No releases found in release group.', + context: { apiName: this.apiName, id }, + }); + } + + // Fetch recordings for the first release + const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; + Logger.log(`MDB | Fetching release recordings from: ${releaseUrl}`); + + const releaseResponseResult = await fromPromise( + requestUrl({ + url: releaseUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id, releaseId: firstRelease.id }, + }), + ); + if (!releaseResponseResult.ok) { + return err(releaseResponseResult.error); + } + const releaseResponse = releaseResponseResult.value; + + if (releaseResponse.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`, + userMessage: `Received status code ${releaseResponse.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: releaseResponse.status, id, releaseId: firstRelease.id }, + }); + } + + const releaseData = (await releaseResponse.json) as MediaResponse; + const tracks = extractTracksFromMedia(releaseData.media); + + // Calculate total album length for the first release + const totalrawLength = + releaseData.media[0]?.tracks.reduce((sum, track) => { + const len = track.length ?? track.recording?.length; + return typeof len === 'number' && !isNaN(len) ? sum + len : sum; + }, 0) ?? 0; + const albumLengthCalc = millisecondsToMinutes(totalrawLength); + + Logger.debug(releaseData); + + return ok( + new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: new Date(result['first-release-date']).getFullYear().toString(), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', + genres: result.genres.map(g => g.name), + subType: result['primary-type'], + albumDuration: albumLengthCalc, + trackCount: releaseData.media[0]?.['track-count'] ?? 0, + tracks: tracks, + rating: result.rating.value * 2, + + userData: { + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; + } +} + +function extractTracksFromMedia(media: MediaResponse['media']): { + number: number; + title: string; + duration: string; + featuredArtists: string[]; +}[] { + if (!media || media.length === 0 || !media[0].tracks) return []; + + return media[0].tracks.map((track, index) => { + const title = track.title ?? track.recording?.title ?? 'Unknown Title'; + const rawLength = track.length ?? track.recording?.length; + const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; + const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; + + return { + number: index + 1, + title, + duration, + featuredArtists, + }; + }); +} + +function millisecondsToMinutes(milliseconds: number): string { + const minutes = Math.floor(milliseconds / 60000); + const seconds = Math.floor((milliseconds % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} diff --git a/packages/obsidian/src/api/apis/OMDbAPI.ts b/packages/obsidian/src/api/apis/OMDbAPI.ts new file mode 100644 index 00000000..24393785 --- /dev/null +++ b/packages/obsidian/src/api/apis/OMDbAPI.ts @@ -0,0 +1,388 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface ErrorResponse { + Response: 'False'; + Error: string; +} + +type SearchResponse = + | { + Response: 'True'; + totalResults: string; + Search: { + Title: string; + Year: string; + Poster: string; + imdbID: string; + Type: string; + }[]; + } + | ErrorResponse; + +type IdResponse = + | { + Response: 'True'; + Title: string; + Year: string; + Rated: string; + Released: string; + Runtime: string; + Genre: string; + Director: string; + Writer: string; + Actors: string; + Plot: string; + Language: string; + Country: string; + Awards: string; + Poster: string; + Metascore: string; + imdbRating: string; + imdbVotes: string; + imdbID: string; + Type: string; + DVD: string; + BoxOffice: string; + Production: string; + Website: string; + } + | ErrorResponse; + +export class OMDbAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'DD MMM YYYY'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OMDbAPI'; + this.apiDescription = 'A free API for Movies, Series and Games.'; + this.apiUrl = 'https://www.omdbapi.com/'; + this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('series', 'series'); + this.typeMappings.set('game', 'game'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as SearchResponse | undefined; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.Response === 'False') { + if (data.Error === 'Movie not found!') { + return ok([]); + } + + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received error from ${this.apiName}: ${data.Error}`, + userMessage: `${data.Error}`, + context: { apiName: this.apiName }, + }); + } + if (!data.Search) { + return ok([]); + } + + // console.debug(data.Search); + + const ret: MediaTypeModel[] = []; + + for (const result of data.Search) { + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + continue; + } + if (type === 'movie') { + ret.push( + new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'series') { + ret.push( + new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'game') { + ret.push( + new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const result = response.json as IdResponse | undefined; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + if (result.Response === 'False') { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received error from ${this.apiName}: ${result.Error}`, + userMessage: `${result.Error}`, + context: { apiName: this.apiName, id }, + }); + } + + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + return err({ + kind: AppErrorKind.Validation, + message: `${result.Type.toLowerCase()} is an unsupported type.`, + userMessage: `${result.Type.toLowerCase()} is an unsupported type.`, + context: { apiName: this.apiName, id, type: result.Type }, + }); + } + + if (type === 'movie') { + return ok( + new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + director: result.Director?.split(', '), + writer: result.Writer?.split(', '), + duration: result.Runtime, + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + boxOffice: result.BoxOffice, + ageRating: result.Rated, + premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'series') { + return ok( + new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + writer: result.Writer?.split(', '), + studio: [], + episodes: 0, + duration: result.Runtime, + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + ageRating: result.Rated, + airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } else if (type === 'game') { + return ok( + new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: result.Year, + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + genres: result.Genre?.split(', '), + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + + return err({ + kind: AppErrorKind.Unexpected, + message: `MDB | Unknown media type for id ${id}`, + userMessage: `Unknown media type for id ${id}`, + context: { apiName: this.apiName, id }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OMDbAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts new file mode 100644 index 00000000..6a52bdba --- /dev/null +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -0,0 +1,214 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BookModel } from 'packages/obsidian/src/models/BookModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/OpenLibrary'; + +interface SearchResponse { + editions: { + docs: { + key?: string; + title?: string; + cover_i?: number; + isbn?: string[]; + }[]; + }; + cover_i?: number; + has_fulltext?: boolean; + edition_count?: number; + title?: string; + author_name?: string[]; + first_publish_year?: number; + key: string; + description?: string; + + number_of_pages_median?: number; + isbn?: string[]; + ratings_average?: number; +} + +export class OpenLibraryAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OpenLibraryAPI'; + this.apiDescription = 'A free API for books'; + this.apiUrl = 'https://openlibrary.org/'; + this.types = [MediaType.Book]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const responseResult = await fromPromise( + client.GET('/search.json', { + params: { + query: { + q: title, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data as { + docs: SearchResponse[]; + }; + + // console.debug(data); + + const ret: MediaTypeModel[] = []; + + for (const result of data.docs) { + ret.push( + new BookModel({ + title: result.title, + englishTitle: result.title, + year: result.first_publish_year?.toString() ?? 'unknown', + dataSource: this.apiName, + id: result.key, + author: result.author_name?.join(', '), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const responseResult = await fromPromise( + client.GET('/search.json', { + params: { + query: { + q: `${id}`, + fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.error !== undefined) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const data = response.data as { + docs: SearchResponse[]; + q?: string; + }; + + const result = data.docs?.[0]; + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data found for ID ${id} in ${this.apiName}.`, + userMessage: `No data found for ID ${id}.`, + context: { apiName: this.apiName, id }, + }); + } + + let key = result.key; + let title = result.title; + let cover_i = result.cover_i; + let isbnArr = result.isbn; + + // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present + const q = data.q ?? ''; + if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { + const edition = result.editions.docs[0]; + key = edition.key ?? key; + title = edition.title ?? title; + cover_i = edition.cover_i ?? cover_i; + isbnArr = edition.isbn ?? isbnArr; + } + + const pages = Number(result.number_of_pages_median); + const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); + const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); + + return ok( + new BookModel({ + title: title, + year: result.first_publish_year?.toString() ?? 'unknown', + dataSource: this.apiName, + url: `https://openlibrary.org` + key, + id: key, + isbn: Number.isNaN(isbn) ? undefined : isbn, + isbn13: Number.isNaN(isbn13) ? undefined : isbn13, + englishTitle: title, + + author: result.author_name?.join(', '), + plot: result.description ?? undefined, + pages: Number.isNaN(pages) ? undefined : pages, + onlineRating: result.ratings_average, + image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, + + released: true, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/RAWGAPI.ts b/packages/obsidian/src/api/apis/RAWGAPI.ts new file mode 100644 index 00000000..21d603ba --- /dev/null +++ b/packages/obsidian/src/api/apis/RAWGAPI.ts @@ -0,0 +1,161 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface RAWGGame { + id: number; + name: string; + released?: string; + background_image?: string; + name_original?: string; + website?: string; + slug?: string; + metacritic?: number; + developers?: { name: string }[]; + publishers?: { name: string }[]; + genres?: { name: string }[]; +} + +interface RAWGSearchResponse { + results: RAWGGame[]; +} + +export class RAWGAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'RAWGAPI'; + this.apiDescription = 'A large open video game database.'; + this.apiUrl = 'https://api.rawg.io/api'; + this.types = [MediaType.Game]; + } + + async searchByTitle(title: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Error ${response.status} from ${this.apiName}.`, + userMessage: `Error ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status }, + }); + } + + const data = response.json as RAWGSearchResponse; + return ok( + data.results.map( + result => + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, + id: result.id.toString(), + image: result.background_image, + }), + ), + ); + } + + async getById(id: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const responseResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}/games/${id}?key=${key}`, + method: 'GET', + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + if (response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Error ${response.status} from ${this.apiName}.`, + userMessage: `Error ${response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.status, id }, + }); + } + + const result = response.json as RAWGGame; + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name_original ?? result.name, + year: result.released ? new Date(result.released).getFullYear().toString() : '', + dataSource: this.apiName, + url: result.website ?? `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), + developers: result.developers?.map(d => d.name) ?? [], + publishers: result.publishers?.map(p => p.name) ?? [], + genres: result.genres?.map(g => g.name) ?? [], + onlineRating: result.metacritic, + image: result.background_image, + released: result.released != null, + releaseDate: result.released, + userData: { played: false, personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; + } +} diff --git a/src/api/apis/SteamAPI.ts b/packages/obsidian/src/api/apis/SteamAPI.ts similarity index 50% rename from src/api/apis/SteamAPI.ts rename to packages/obsidian/src/api/apis/SteamAPI.ts index b8e72b25..8a47efa3 100644 --- a/src/api/apis/SteamAPI.ts +++ b/packages/obsidian/src/api/apis/SteamAPI.ts @@ -1,10 +1,15 @@ import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { imageUrlExists } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { imageUrlExists } from 'packages/obsidian/src/utils/Utils'; interface SearchResponse { appid: string; @@ -143,16 +148,34 @@ export class SteamAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); } const data = (await fetchData.json) as SearchResponse[]; @@ -174,19 +197,37 @@ export class SteamAPI extends APIModel { ); } - return ret; + return ok(ret); } - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; - const fetchData = await requestUrl({ - url: searchUrl, - }); + const fetchDataResult = await fromPromise( + requestUrl({ + url: searchUrl, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); } // console.debug(await fetchData.json); @@ -200,14 +241,27 @@ export class SteamAPI extends APIModel { } } if (!result) { - throw Error(`MDB | API returned invalid data.`); + return err({ + kind: AppErrorKind.Api, + message: 'MDB | API returned invalid data.', + userMessage: 'Steam returned invalid data.', + context: { apiName: this.apiName, id }, + }); } // console.debug(result); // Check if a poster version of the image exists, else use the header image const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; - const exists = await imageUrlExists(imageUrl); + const existsResult = await fromPromise(imageUrlExists(imageUrl), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to validate image URL for ${this.apiName}`, + userMessage: `Failed to validate image URL for ${this.apiName}`, + context: { apiName: this.apiName, id, imageUrl }, + }), + ); + const exists = existsResult.ok ? existsResult.value : false; let finalimageurl; if (exists) { finalimageurl = imageUrl; @@ -215,29 +269,31 @@ export class SteamAPI extends APIModel { finalimageurl = result.header_image ?? ''; } - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: new Date(result.release_date.date).getFullYear().toString(), - dataSource: this.apiName, - url: `https://store.steampowered.com/app/${result.steam_appid}`, - id: result.steam_appid.toString(), - - developers: result.developers, - publishers: result.publishers, - genres: result.genres?.map(x => x.description), - onlineRating: result.metacritic?.score, - image: finalimageurl, - - released: !result.release_date?.coming_soon, - releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), - - userData: { - played: false, - personalRating: 0, - }, - }); + return ok( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: new Date(result.release_date.date).getFullYear().toString(), + dataSource: this.apiName, + url: `https://store.steampowered.com/app/${result.steam_appid}`, + id: result.steam_appid.toString(), + + developers: result.developers, + publishers: result.publishers, + genres: result.genres?.map(x => x.description), + onlineRating: result.metacritic?.score, + image: finalimageurl, + + released: !result.release_date?.coming_soon, + releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + }), + ); } getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.SteamAPI_disabledMediaTypes; diff --git a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts new file mode 100644 index 00000000..9f2dbf15 --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts @@ -0,0 +1,265 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/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; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBMovieAPI'; + this.apiDescription = 'A community built Movie DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Movie]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/search/movie', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.total_results === 0 || !data.results) { + return ok([]); + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new MovieModel({ + type: 'movie', + title: result.original_title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/movie/{movie_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { movie_id: parseInt(id) }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // console.debug(result); + const credits = (result as TMDBCreditsResponse).credits; + + return ok( + new MovieModel({ + type: 'movie', + title: result.title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/movie/${result.id}`, + id: result.id.toString(), + + plot: result.overview ?? '', + genres: result.genres?.map(g => 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, + actors: getTopCastNames(credits, 5), + image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, + + released: ['Released'].includes(result.status!), + streamingServices: [], + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts new file mode 100644 index 00000000..f2710e3e --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -0,0 +1,440 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/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; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBSeasonAPI'; + this.apiDescription = 'A community built Series DB (seasons).'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Season]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'season'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const searchResponseResult = await fromPromise( + client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!searchResponseResult.ok) { + return err(searchResponseResult.error); + } + const searchResponse = searchResponseResult.value; + + if (searchResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + + if (searchResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${searchResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: searchResponse.response.status }, + }); + } + + const searchData = searchResponse.data; + + if (!searchData?.results || searchData.total_results === 0) { + return ok([]); + } + + const topResults = searchData.results.slice(0, 20); + + const items = 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: obsidianFetch, + }); + + if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) { + totalSeasons = detailsResponse.data.seasons.length; + } + } catch { + // Ignore detail errors and use 0 as fallback. + } + } + + 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', + dataSource: this.apiName, + id: result.id?.toString() ?? '', + seasonTitle: result.name ?? result.original_name ?? '', + seasonNumber: totalSeasons, + image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', + }); + }), + ); + + return ok(items); + } + + // Fetch all seasons for a given series + async getSeasonsForSeries(tvId: string): Promise> { + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, tvId }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const seriesResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: Number.parseInt(tvId, 10) }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, tvId }, + }), + ); + + if (!seriesResponseResult.ok) { + return err(seriesResponseResult.error); + } + const seriesResponse = seriesResponseResult.value; + + if (seriesResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, tvId }, + }); + } + + if (seriesResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seriesResponse.response.status, tvId }, + }); + } + + const seriesData = seriesResponse.data; + const seriesName = seriesData?.name ?? ''; + + const ret: SeasonModel[] = []; + + if (Array.isArray(seriesData?.seasons)) { + for (const season of seriesData.seasons) { + const seasonNumber = season.season_number ?? 0; + const titleText = `${seriesName} - Season ${seasonNumber}`; + + ret.push( + new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: `${tvId}/season/${seasonNumber}`, + seasonTitle: season.name ?? titleText, + seasonNumber: seasonNumber, + image: season.poster_path ?? '', + }), + ); + } + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + // Expect season ids like "12345/season/2" + const m = /^(\d+)\/season\/(\d+)$/.exec(id); + if (!m) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Invalid season id "${id}". Expected format "/season/".`, + userMessage: `Invalid season id "${id}".`, + context: { apiName: this.apiName, id }, + }); + } + + 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 seasonResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}/season/{season_number}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { + series_id: tvId, + season_number: seasonNumber, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!seasonResponseResult.ok) { + return err(seasonResponseResult.error); + } + const seasonResponse = seasonResponseResult.value; + + if (seasonResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + + if (seasonResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seasonResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seasonResponse.response.status, id }, + }); + } + + const seasonData = seasonResponse.data; + if (!seasonData) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // Fetch parent series to build consistent titles and inherit fields + const seriesResponseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: tvId }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!seriesResponseResult.ok) { + return err(seriesResponseResult.error); + } + const seriesResponse = seriesResponseResult.value; + + if (seriesResponse.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + + if (seriesResponse.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: seriesResponse.response.status, id }, + }); + } + + const seriesData = seriesResponse.data; + + if (!seriesData) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + const seriesName = seriesData?.name ?? ''; + const airDate = seasonData.air_date ?? ''; + const titleText = `${seriesName} - Season ${seasonData.season_number}`; + + // Get airedTo as the air_date of the last episode, if available + let airedTo = 'unknown'; + if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { + const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; + if (lastEp?.air_date) airedTo = lastEp.air_date; + } + const formattedAiredTo = airedTo === 'unknown' ? 'unknown' : (this.plugin.dateFormatter.format(airedTo, this.apiDateFormat) ?? airedTo); + + return ok( + new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + 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 ?? seasonNumber, + episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, + airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', + airedTo: formattedAiredTo, + plot: seasonData.overview ?? '', + image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', + 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, + actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits), + released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), + streamingServices: [], + airing: ['Returning Series'].includes(seriesData.status ?? ''), + userData: { watched: false, lastWatched: '', personalRating: 0 }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return []; + } +} diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts new file mode 100644 index 00000000..40f68d7c --- /dev/null +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -0,0 +1,255 @@ +import createClient from 'openapi-fetch'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; +import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; +import type { paths } from 'packages/schemas/src/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; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBSeriesAPI'; + this.apiDescription = 'A community built Series DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'series'); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, title }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status }, + }); + } + + const data = response.data; + + if (!data) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName }, + }); + } + + if (data.total_results === 0 || !data.results) { + return ok([]); + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); + + if (!key) { + return err({ + kind: AppErrorKind.Validation, + message: `MDB | API key for ${this.apiName} missing.`, + userMessage: `API key for ${this.apiName} missing.`, + context: { apiName: this.apiName, id }, + }); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const responseResult = await fromPromise( + client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${key}`, + }, + params: { + path: { series_id: parseInt(id) }, + query: { + append_to_response: 'credits', + }, + }, + fetch: obsidianFetch, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + + if (!responseResult.ok) { + return err(responseResult.error); + } + const response = responseResult.value; + + if (response.response.status === 401) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, + userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, + context: { apiName: this.apiName, id }, + }); + } + if (response.response.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, id }, + }); + } + + const result = response.data; + + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + // console.debug(result); + const credits = (result as TMDBCreditsResponse).credits; + + return ok( + new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${result.id}`, + id: result.id.toString(), + + plot: result.overview ?? '', + 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, + 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!), + streamingServices: [], + airing: ['Returning Series'].includes(result.status!), + airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown', + airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/VNDBAPI.ts b/packages/obsidian/src/api/apis/VNDBAPI.ts new file mode 100644 index 00000000..eae27de9 --- /dev/null +++ b/packages/obsidian/src/api/apis/VNDBAPI.ts @@ -0,0 +1,334 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +enum VNDevStatus { + Finished, + InDevelopment, + Cancelled, +} + +enum TagSpoiler { + None, + Minor, + Major, +} + +enum TagCategory { + Content = 'cont', + Sexual = 'ero', + Technical = 'tech', +} + +/** + * A partial `POST /vn` response payload; desired fields should be listed in the request body. + */ +interface VNJSONResponse { + more: boolean; + results: [ + { + id: string; + title: string; + titles: [ + { + title: string; + lang: string; + }, + ]; + devstatus: VNDevStatus; + released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents + image: { + url: string; + sexual: number; + } | null; + rating: number | null; + tags: [ + { + id: string; + name: string; + category: TagCategory; + rating: number; + spoiler: TagSpoiler; + }, + ]; + developers: [ + { + id: string; + name: string; + }, + ]; + }, + ]; +} + +/** + * A partial `POST /release` response payload; desired fields should be listed in the request body. + */ +interface ReleaseJSONResponse { + more: boolean; + results: [ + { + id: string; + producers: [ + { + id: string; + name: string; + developer: boolean; + publisher: boolean; + }, + ]; + }, + ]; +} + +export class VNDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'VNDB API'; + this.apiDescription = 'A free API for visual novels.'; + this.apiUrl = 'https://api.vndb.org/kana'; + this.types = [MediaType.Game]; + } + + /** + * Make a `POST` request to the VNDB API. + * @param endpoint The API endpoint to query. E.g. "/vn". + * @param body A JSON object defining the query, following the VNDB API structure. + * @returns A JSON object representing the query response. + * @see {@link https://api.vndb.org/kana#api-structure} + */ + private async postQuery(endpoint: string, body: string): Promise> { + const fetchDataResult = await fromPromise( + requestUrl({ + url: `${this.apiUrl}${endpoint}`, + method: 'POST', + contentType: 'application/json', + body: body, + throw: false, + }), + cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, endpoint }, + }), + ); + if (!fetchDataResult.ok) { + return err(fetchDataResult.error); + } + const fetchData = fetchDataResult.value; + + if (fetchData.status !== 200) { + switch (fetchData.status) { + case 400: + return err({ + kind: AppErrorKind.Validation, + message: `MDB | Invalid request body or query [${fetchData.text}].`, + userMessage: 'Invalid VNDB request.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 404: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | Invalid API path or HTTP method.', + userMessage: 'VNDB endpoint not found.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 429: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB throttled the request.', + userMessage: 'VNDB throttled the request. Please try again later.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 500: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB server error.', + userMessage: 'VNDB server error.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + case 502: + return err({ + kind: AppErrorKind.Api, + message: 'MDB | VNDB server is down.', + userMessage: 'VNDB server is down.', + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + default: + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, endpoint, status: fetchData.status }, + }); + } + } + + return ok(fetchData.json); + } + + /** + * Make a `POST` request to the `/vn` endpoint. + * Queries visual novel entries. + * @see {@link https://api.vndb.org/kana#post-vn} + */ + private async postVNQuery(body: string): Promise> { + const result = await this.postQuery('/vn', body); + return result.ok ? ok(result.value as VNJSONResponse) : err(result.error); + } + + /** + * Make a `POST` request to the `/release` endpoint. + * Queries release entries. + * @see {@link https://api.vndb.org/kana#post-release} + */ + private async postReleaseQuery(body: string): Promise> { + const result = await this.postQuery('/release', body); + return result.ok ? ok(result.value as ReleaseJSONResponse) : err(result.error); + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + /* SFW Filter: has ANY official&&complete&&standalone&&SFW release + OR has NO official&&standalone&&NSFW release + OR has the `In-game Sexual Content Toggle` (g2708) tag */ + // prettier-ignore + const vnDataResult = await this.postVNQuery(`{ + "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : + `, ["or" + , ["release", "=", ["and" + , ["official", "=", "1"] + , ["rtype", "=", "complete"] + , ["patch", "!=", "1"] + , ["has_ero", "!=", "1"] + ]] + , ["release", "!=", ["and" + , ["official", "=", "1"] + , ["patch", "!=", "1"] + , ["has_ero", "=", "1"] + ]] + , ["tag", "=", "g2708"] + ]`} + , ["search", "=", "${title}"] + ], + "fields": "title, titles{title, lang}, released", + "sort": "searchrank", + "results": 20 + }`); + if (!vnDataResult.ok) { + return err(vnDataResult.error); + } + const vnData = vnDataResult.value; + + const ret: MediaTypeModel[] = []; + for (const vn of vnData.results) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + dataSource: this.apiName, + id: vn.id, + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const vnDataResult = await this.postVNQuery(`{ + "filters": ["id", "=", "${id}"], + "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" + }`); + if (!vnDataResult.ok) { + return err(vnDataResult.error); + } + const vnData = vnDataResult.value; + + if (vnData.results.length !== 1) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Expected 1 result from query, got ${vnData.results.length}.`, + userMessage: 'Unexpected VNDB response.', + context: { apiName: this.apiName, id }, + }); + } + const vn = vnData.results[0]; + const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; + vn.released ??= 'Unknown'; + + const releaseDataResult = await this.postReleaseQuery(`{ + "filters": ["and" + , ["vn", "=" + , ["id", "=", "${id}"] + ] + , ["official", "=", 1] + ], + "fields": "producers.name, producers.publisher, producers.developer", + "results": 100 + }`); + if (!releaseDataResult.ok) { + return err(releaseDataResult.error); + } + const releaseData = releaseDataResult.value; + + return ok( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, + dataSource: this.apiName, + url: `https://vndb.org/${vn.id}`, + id: vn.id, + + developers: vn.developers.map(d => d.name), + publishers: releaseData.results + .flatMap(r => r.producers) + .filter(p => p.publisher) + .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list + .map(p => p.name) + .unique(), + genres: vn.tags + .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) + .sort((t1, t2) => t2.rating - t1.rating) + .map(t => t.name), + onlineRating: vn.rating ?? NaN, + // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively + image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, + + released: vn.devstatus === VNDevStatus.Finished, + releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, + + userData: { + played: false, + personalRating: 0, + }, + }), + ); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.VNDBAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/api/apis/WikipediaAPI.ts b/packages/obsidian/src/api/apis/WikipediaAPI.ts new file mode 100644 index 00000000..27657860 --- /dev/null +++ b/packages/obsidian/src/api/apis/WikipediaAPI.ts @@ -0,0 +1,170 @@ +import { requestUrl } from 'obsidian'; +import { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; + +interface SearchResponse { + query: { + search: { + title: string; + pageid: number; + }[]; + }; +} + +interface IdResponse { + query: { + pages: Record; + }; +} + +interface WikipediaPage { + pageid: number; + title: string; + contentmodel: string; + pagelanguage: string; + pagelanguagehtmlcode: string; + pagelanguagedir: string; + touched: string; // ISO date string + lastrevid: number; + length: number; + fullurl: string; + editurl: string; + canonicalurl: string; +} +export class WikipediaAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'Wikipedia API'; + this.apiDescription = 'The API behind Wikipedia'; + this.apiUrl = 'https://www.wikipedia.com'; + this.types = [MediaType.Wiki]; + } + + async searchByTitle(title: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; + const fetchData = await requestUrl({ + url: searchUrl, + method: 'GET', + throw: false, + }); + // console.debug(fetchData); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status }, + }); + } + + const response = fetchData as { status: number; json(): Promise }; + const dataResult = await fromPromise(response.json(), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to parse response from ${this.apiName}`, + userMessage: `Failed to parse response from ${this.apiName}`, + context: { apiName: this.apiName }, + }), + ); + if (!dataResult.ok) { + return err(dataResult.error); + } + const data = dataResult.value as SearchResponse; + Logger.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data.query.search) { + ret.push( + new WikiModel({ + type: 'wiki', + title: result.title, + englishTitle: result.title, + year: '', + dataSource: this.apiName, + id: result.pageid.toString(), + }), + ); + } + + return ok(ret); + } + + async getById(id: string): Promise> { + Logger.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; + const fetchData = await requestUrl({ + url: searchUrl, + method: 'GET', + throw: false, + }); + + if (fetchData.status !== 200) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, + userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: fetchData.status, id }, + }); + } + + const response = fetchData as { status: number; json(): Promise }; + const dataResult = await fromPromise(response.json(), cause => + toAppError(cause, { + kind: AppErrorKind.Network, + message: `MDB | Failed to parse response from ${this.apiName}`, + userMessage: `Failed to parse response from ${this.apiName}`, + context: { apiName: this.apiName, id }, + }), + ); + if (!dataResult.ok) { + return err(dataResult.error); + } + const data = dataResult.value as IdResponse; + // console.debug(data); + const result = Object.values(data?.query?.pages)[0]; + if (!result) { + return err({ + kind: AppErrorKind.Api, + message: `MDB | No data received from ${this.apiName}.`, + userMessage: `No data received from ${this.apiName}.`, + context: { apiName: this.apiName, id }, + }); + } + + return ok( + new WikiModel({ + title: result.title, + englishTitle: result.title, + dataSource: this.apiName, + url: result.fullurl, + id: result.pageid.toString(), + + wikiUrl: result.fullurl, + lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), + length: result.length, + + userData: {}, + }), + ); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.WikipediaAPI_disabledMediaTypes; + } +} diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts new file mode 100644 index 00000000..3bddc83c --- /dev/null +++ b/packages/obsidian/src/main.ts @@ -0,0 +1,209 @@ +import 'packages/obsidian/src/styles.css'; +import { Plugin, TFolder } from 'obsidian'; +import { APIManager } from 'packages/obsidian/src/api/APIManager'; +import { BoardGameGeekAPI } from 'packages/obsidian/src/api/apis/BoardGameGeekAPI'; +import { ComicVineAPI } from 'packages/obsidian/src/api/apis/ComicVineAPI'; +import { IGDBAPI } from 'packages/obsidian/src/api/apis/IGDBAPI'; +import { MALAPI } from 'packages/obsidian/src/api/apis/MALAPI'; +import { MALAPIManga } from 'packages/obsidian/src/api/apis/MALAPIManga'; +import { MusicBrainzAPI } from 'packages/obsidian/src/api/apis/MusicBrainzAPI'; +import { OMDbAPI } from 'packages/obsidian/src/api/apis/OMDbAPI'; +import { OpenLibraryAPI } from 'packages/obsidian/src/api/apis/OpenLibraryAPI'; +import { RAWGAPI } from 'packages/obsidian/src/api/apis/RAWGAPI'; +import { SteamAPI } from 'packages/obsidian/src/api/apis/SteamAPI'; +import { TMDBMovieAPI } from 'packages/obsidian/src/api/apis/TMDBMovieAPI'; +import { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import { TMDBSeriesAPI } from 'packages/obsidian/src/api/apis/TMDBSeriesAPI'; +import { VNDBAPI } from 'packages/obsidian/src/api/apis/VNDBAPI'; +import { WikipediaAPI } from 'packages/obsidian/src/api/apis/WikipediaAPI'; +import { PropertyMapper } from 'packages/obsidian/src/settings/PropertyMapper'; +import { PropertyMappingModel } from 'packages/obsidian/src/settings/PropertyMapping'; +import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings'; +import { MediaDbSettingTab } from 'packages/obsidian/src/settings/Settings'; +import { getDefaultSettings } from 'packages/obsidian/src/settings/Settings'; +import { BulkImportHelper } from 'packages/obsidian/src/utils/BulkImportHelper'; +import { DateFormatter } from 'packages/obsidian/src/utils/DateFormatter'; +import { ErrorReporter } from 'packages/obsidian/src/utils/ErrorReporter'; +import { MediaDbEntryHelper } from 'packages/obsidian/src/utils/MediaDbEntryHelper'; +import { MediaDbFileHelper } from 'packages/obsidian/src/utils/MediaDbFileHelper'; +import { MediaTypeManager } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import { ModalHelper } from 'packages/obsidian/src/utils/ModalHelper'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; + +export default class MediaDbPlugin extends Plugin { + settings!: MediaDbPluginSettings; + apiManager!: APIManager; + mediaTypeManager!: MediaTypeManager; + modelPropertyMapper!: PropertyMapper; + modalHelper!: ModalHelper; + fileHelper!: MediaDbFileHelper; + entryHelper!: MediaDbEntryHelper; + bulkImportHelper!: BulkImportHelper; + dateFormatter!: DateFormatter; + errorReporter!: ErrorReporter; + + async onload(): Promise { + this.mediaTypeManager = new MediaTypeManager(); + this.modelPropertyMapper = new PropertyMapper(this); + this.errorReporter = new ErrorReporter(); + this.modalHelper = new ModalHelper(this); + this.fileHelper = new MediaDbFileHelper(this); + this.entryHelper = new MediaDbEntryHelper(this); + this.bulkImportHelper = new BulkImportHelper(this); + this.dateFormatter = new DateFormatter(); + + await this.loadSettings(); + this.registerDefaultApis(); + this.addSettingTab(new MediaDbSettingTab(this.app, this)); + this.registerRibbonAndFileMenu(); + this.registerCommands(); + } + + onunload(): void {} + + private registerDefaultApis(): void { + this.apiManager = new APIManager(); + this.apiManager.registerAPI(new OMDbAPI(this)); + this.apiManager.registerAPI(new MALAPI(this)); + this.apiManager.registerAPI(new MALAPIManga(this)); + this.apiManager.registerAPI(new WikipediaAPI(this)); + this.apiManager.registerAPI(new MusicBrainzAPI(this)); + this.apiManager.registerAPI(new SteamAPI(this)); + this.apiManager.registerAPI(new TMDBSeriesAPI(this)); + this.apiManager.registerAPI(new TMDBSeasonAPI(this)); + this.apiManager.registerAPI(new TMDBMovieAPI(this)); + this.apiManager.registerAPI(new BoardGameGeekAPI(this)); + this.apiManager.registerAPI(new OpenLibraryAPI(this)); + this.apiManager.registerAPI(new ComicVineAPI(this)); + this.apiManager.registerAPI(new IGDBAPI(this)); + this.apiManager.registerAPI(new RAWGAPI(this)); + this.apiManager.registerAPI(new VNDBAPI(this)); + } + + private registerRibbonAndFileMenu(): void { + const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.entryHelper.createEntryWithAdvancedSearchModal()); + ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); + + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file) => { + if (file instanceof TFolder) { + menu.addItem(item => { + item.setTitle('Import folder as Media DB entries') + .setIcon('database') + .onClick(() => this.bulkImportHelper.import(file)); + }); + } + }), + ); + } + + private registerCommands(): void { + this.addCommand({ + id: 'open-media-db-search-modal', + name: 'Create entry', + callback: () => this.entryHelper.createEntryWithSearchModal(), + }); + + for (const mediaType of MEDIA_TYPES) { + this.addCommand({ + id: `open-media-db-search-modal-with-${mediaType}`, + name: `Create entry (${unCamelCase(mediaType)})`, + callback: () => this.entryHelper.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), + }); + } + + this.addCommand({ + id: 'open-media-db-advanced-search-modal', + name: 'Create entry (advanced search)', + callback: () => this.entryHelper.createEntryWithAdvancedSearchModal(), + }); + + this.addCommand({ + id: 'open-media-db-id-search-modal', + name: 'Create entry by id', + callback: () => this.entryHelper.createEntryWithIdSearchModal(), + }); + + this.addCommand({ + id: 'update-media-db-note', + name: 'Update open note (this will recreate the note)', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.fileHelper.updateActiveNote(false); + } + return true; + }, + }); + + this.addCommand({ + id: 'update-media-db-note-metadata', + name: 'Update metadata', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.fileHelper.updateActiveNote(true); + } + return true; + }, + }); + + this.addCommand({ + id: 'add-media-db-link', + name: 'Insert link', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.entryHelper.createLinkWithSearchModal(); + } + return true; + }, + }); + } + + async loadSettings(): Promise { + const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; + 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; + + const migratedModels = PropertyMappingModel.migrateModels( + loadedSettings.propertyMappingModels || [], + defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), + ); + + loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); + + this.settings = loadedSettings; + + await this.saveSettings(); + } + + async saveSettings(): Promise { + this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); + this.dateFormatter.setFormat(this.settings.customDateFormat); + + await this.saveData(this.settings); + } +} diff --git a/src/modals/ConfirmOverwriteModal.ts b/packages/obsidian/src/modals/ConfirmOverwriteModal.ts similarity index 100% rename from src/modals/ConfirmOverwriteModal.ts rename to packages/obsidian/src/modals/ConfirmOverwriteModal.ts diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts similarity index 86% rename from src/modals/MediaDbAdvancedSearchModal.ts rename to packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index e0e80207..04f0598d 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -1,8 +1,8 @@ import type { ButtonComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; -import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbAdvancedSearchModal extends Modal { plugin: MediaDbPlugin; @@ -71,7 +71,7 @@ export class MediaDbAdvancedSearchModal extends Modal { const placeholder = 'Search by title'; const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); @@ -85,13 +85,13 @@ export class MediaDbAdvancedSearchModal extends Modal { // const apiToggleComponents: Component[] = []; for (const api of this.plugin.apiManager.apis) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' }); + const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createSpan({ text: api.apiName, cls: 'media-db-plugin-list-text' }); apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' }); - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(api.apiName); diff --git a/src/modals/MediaDbBulkImportModal.ts b/packages/obsidian/src/modals/MediaDbBulkImportModal.ts similarity index 79% rename from src/modals/MediaDbBulkImportModal.ts rename to packages/obsidian/src/modals/MediaDbBulkImportModal.ts index 0763d32e..0b5c3d4d 100644 --- a/src/modals/MediaDbBulkImportModal.ts +++ b/packages/obsidian/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 '../api/APIModel'; -import type MediaDbPlugin from '../main'; -import { BulkImportLookupMethod } from '../utils/BulkImportHelper'; +import type { APIModel } from 'packages/obsidian/src/api/APIModel'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { BulkImportLookupMethod } from 'packages/obsidian/src/utils/BulkImportHelper'; export class MediaDbBulkImportModal extends Modal { plugin: MediaDbPlugin; @@ -47,14 +47,14 @@ export class MediaDbBulkImportModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' }); - const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - appendContentToggleTextWrapper.createEl('span', { + const appendContentToggleElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + appendContentToggleTextWrapper.createSpan({ text: 'If this is enabled, the plugin will override metadata fields with the same name.', cls: 'media-db-plugin-list-text', }); - const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper); appendContentToggle.setValue(false); @@ -81,9 +81,9 @@ export class MediaDbBulkImportModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' }); + const fieldNameWrapperEl = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const fieldNameLabelWrapperEl = fieldNameWrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + fieldNameLabelWrapperEl.createSpan({ text: 'Using the property named', cls: 'media-db-plugin-list-text' }); const fieldNameComponent = new TextComponent(fieldNameWrapperEl); fieldNameComponent.setPlaceholder('title / id'); @@ -115,9 +115,9 @@ export class MediaDbBulkImportModal extends Modal { } createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void { - const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' }); + const wrapperEl = parentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const labelWrapperEl = wrapperEl.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + labelWrapperEl.createSpan({ text: label, cls: 'media-db-plugin-list-text' }); const dropDownComponent = new DropdownComponent(wrapperEl); dropDownComponent.onChange(onChange); diff --git a/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts similarity index 82% rename from src/modals/MediaDbIdSearchModal.ts rename to packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 7027bb52..1f65d30e 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -1,8 +1,8 @@ import type { ButtonComponent } from 'obsidian'; import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; -import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { IdSearchModalData, IdSearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbIdSearchModal extends Modal { plugin: MediaDbPlugin; @@ -69,7 +69,7 @@ export class MediaDbIdSearchModal extends Modal { const placeholder = 'Search by id'; const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.onChange(value => (this.query = value)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); @@ -79,9 +79,9 @@ export class MediaDbIdSearchModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' }); + const apiSelectorWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); + const apiSelectorTExtWrapper = apiSelectorWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiSelectorTExtWrapper.createSpan({ text: 'API to search', cls: 'media-db-plugin-list-text' }); const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper); apiSelectorComponent.onChange((value: string) => { diff --git a/src/modals/MediaDbPreviewModal.ts b/packages/obsidian/src/modals/MediaDbPreviewModal.ts similarity index 80% rename from src/modals/MediaDbPreviewModal.ts rename to packages/obsidian/src/modals/MediaDbPreviewModal.ts index 1efd66a1..6bf3848f 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/packages/obsidian/src/modals/MediaDbPreviewModal.ts @@ -1,8 +1,9 @@ import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; -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'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { PreviewModalData, PreviewModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { PREVIEW_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; export class MediaDbPreviewModal extends Modal { plugin: MediaDbPlugin; @@ -48,14 +49,14 @@ export class MediaDbPreviewModal extends Modal { previewWrapper.createEl('h3', { text: result.englishTitle }); const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' }); - let fileContent = this.plugin.generateMediaDbNoteFrontmatterPreview(result); + let fileContent = this.plugin.fileHelper.generateMediaDbNoteFrontmatterPreview(result); fileContent = `\`\`\`yaml\n${fileContent}\`\`\``; try { // TODO: fix this not rendering the frontmatter any more await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent); } catch (e) { - console.warn(`mdb | error during rendering of preview`, e); + Logger.warn(`mdb | error during rendering of preview`, e); } } diff --git a/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts similarity index 82% rename from src/modals/MediaDbSearchModal.ts rename to packages/obsidian/src/modals/MediaDbSearchModal.ts index 800eacee..e62828d8 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -1,11 +1,11 @@ import type { ButtonComponent } from 'obsidian'; import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; -import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; -import { unCamelCase } from '../utils/Utils'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; +import type { SearchModalData, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { SEARCH_MODAL_DEFAULT_OPTIONS } from 'packages/obsidian/src/utils/ModalHelper'; +import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; export class MediaDbSearchModal extends Modal { plugin: MediaDbPlugin; @@ -76,7 +76,7 @@ export class MediaDbSearchModal extends Modal { const searchComponent = new TextComponent(contentEl); let currentToggle: ToggleComponent | undefined = undefined; - searchComponent.inputEl.style.width = '100%'; + searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); @@ -89,12 +89,12 @@ export class MediaDbSearchModal extends Modal { contentEl.createEl('h3', { text: 'APIs to search' }); for (const mediaType of MEDIA_TYPES) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const apiToggleListElementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-list-wrapper' }); - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); + const apiToggleTextWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createSpan({ text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + const apiToggleComponentWrapper = apiToggleListElementWrapper.createDiv({ cls: 'media-db-plugin-list-toggle' }); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(unCamelCase(mediaType)); diff --git a/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts similarity index 90% rename from src/modals/MediaDbSearchResultModal.ts rename to packages/obsidian/src/modals/MediaDbSearchResultModal.ts index e258b95c..1cc035ff 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -1,9 +1,10 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; -import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; -import { MediaItemComponent } from './MediaItemComponent'; -import { SelectModal } from './SelectModal'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper'; +import { MediaItemComponent } from 'packages/obsidian/src/Modals/MediaItemComponent'; + export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts similarity index 88% rename from src/modals/MediaDbSeasonSelectModal.ts rename to packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index be3a1ae5..4a72f09a 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -1,6 +1,6 @@ -import type MediaDbPlugin from '../main'; -import { MediaItemComponent } from './MediaItemComponent'; -import { SelectModal } from './SelectModal'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; +import { MediaItemComponent } from 'packages/obsidian/src/Modals/MediaItemComponent'; export interface SeasonSelectModalElement { season_number: number; diff --git a/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts similarity index 100% rename from src/modals/MediaItemComponent.ts rename to packages/obsidian/src/modals/MediaItemComponent.ts diff --git a/src/modals/SelectModal.ts b/packages/obsidian/src/modals/SelectModal.ts similarity index 95% rename from src/modals/SelectModal.ts rename to packages/obsidian/src/modals/SelectModal.ts index d54b5f99..8d9ef3e6 100644 --- a/src/modals/SelectModal.ts +++ b/packages/obsidian/src/modals/SelectModal.ts @@ -1,7 +1,7 @@ import type { App, ButtonComponent } from 'obsidian'; import { Modal, Setting } from 'obsidian'; -import { mod } from '../utils/Utils'; -import { SelectModalElement } from './SelectModalElement'; +import { SelectModalElement } from 'packages/obsidian/src/modals/SelectModalElement'; +import { mod } from 'packages/obsidian/src/utils/Utils'; export abstract class SelectModal extends Modal { allowMultiSelect: boolean; @@ -48,7 +48,7 @@ export abstract class SelectModal extends Modal { this.activateHighlighted(); }); this.scope.register([], ' ', evt => { - if (this.elementWrapper && this.elementWrapper === document.activeElement) { + if (this.elementWrapper && this.elementWrapper === activeDocument.activeElement) { this.activateHighlighted(); evt.preventDefault(); } diff --git a/src/modals/SelectModalElement.ts b/packages/obsidian/src/modals/SelectModalElement.ts similarity index 96% rename from src/modals/SelectModalElement.ts rename to packages/obsidian/src/modals/SelectModalElement.ts index 70c09f17..7b78d24b 100644 --- a/src/modals/SelectModalElement.ts +++ b/packages/obsidian/src/modals/SelectModalElement.ts @@ -1,4 +1,4 @@ -import type { SelectModal } from './SelectModal'; +import type { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; export class SelectModalElement { selectModal: SelectModal; diff --git a/src/models/BoardGameModel.ts b/packages/obsidian/src/models/BoardGameModel.ts similarity index 78% rename from src/models/BoardGameModel.ts rename to packages/obsidian/src/models/BoardGameModel.ts index e782137d..1297b97b 100644 --- a/src/models/BoardGameModel.ts +++ b/packages/obsidian/src/models/BoardGameModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type BoardGameData = ModelToData; diff --git a/src/models/BookModel.ts b/packages/obsidian/src/models/BookModel.ts similarity index 77% rename from src/models/BookModel.ts rename to packages/obsidian/src/models/BookModel.ts index e75da93d..ff5ba652 100644 --- a/src/models/BookModel.ts +++ b/packages/obsidian/src/models/BookModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type BookData = ModelToData; diff --git a/src/models/ComicMangaModel.ts b/packages/obsidian/src/models/ComicMangaModel.ts similarity index 82% rename from src/models/ComicMangaModel.ts rename to packages/obsidian/src/models/ComicMangaModel.ts index ec47a959..57fc7911 100644 --- a/src/models/ComicMangaModel.ts +++ b/packages/obsidian/src/models/ComicMangaModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type ComicMangaData = ModelToData; diff --git a/src/models/GameModel.ts b/packages/obsidian/src/models/GameModel.ts similarity index 76% rename from src/models/GameModel.ts rename to packages/obsidian/src/models/GameModel.ts index 5661f6ec..536ee139 100644 --- a/src/models/GameModel.ts +++ b/packages/obsidian/src/models/GameModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type GameData = ModelToData; diff --git a/src/models/MediaTypeModel.ts b/packages/obsidian/src/models/MediaTypeModel.ts similarity index 92% rename from src/models/MediaTypeModel.ts rename to packages/obsidian/src/models/MediaTypeModel.ts index e4d03a09..cdadbb7a 100644 --- a/src/models/MediaTypeModel.ts +++ b/packages/obsidian/src/models/MediaTypeModel.ts @@ -1,4 +1,4 @@ -import type { MediaType } from '../utils/MediaType'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; export abstract class MediaTypeModel { type: string; diff --git a/src/models/MovieModel.ts b/packages/obsidian/src/models/MovieModel.ts similarity index 82% rename from src/models/MovieModel.ts rename to packages/obsidian/src/models/MovieModel.ts index 563a0f98..a3866156 100644 --- a/src/models/MovieModel.ts +++ b/packages/obsidian/src/models/MovieModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type MovieData = ModelToData; diff --git a/src/models/MusicReleaseModel.ts b/packages/obsidian/src/models/MusicReleaseModel.ts similarity index 81% rename from src/models/MusicReleaseModel.ts rename to packages/obsidian/src/models/MusicReleaseModel.ts index 38427a91..decb5d54 100644 --- a/src/models/MusicReleaseModel.ts +++ b/packages/obsidian/src/models/MusicReleaseModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type MusicReleaseData = ModelToData; diff --git a/src/models/SeasonModel.ts b/packages/obsidian/src/models/SeasonModel.ts similarity index 82% rename from src/models/SeasonModel.ts rename to packages/obsidian/src/models/SeasonModel.ts index fc4e0a3a..b295d36b 100644 --- a/src/models/SeasonModel.ts +++ b/packages/obsidian/src/models/SeasonModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type SeasonData = ModelToData; diff --git a/src/models/SeriesModel.ts b/packages/obsidian/src/models/SeriesModel.ts similarity index 82% rename from src/models/SeriesModel.ts rename to packages/obsidian/src/models/SeriesModel.ts index b87a32c0..974d0197 100644 --- a/src/models/SeriesModel.ts +++ b/packages/obsidian/src/models/SeriesModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type SeriesData = ModelToData; diff --git a/src/models/WikiModel.ts b/packages/obsidian/src/models/WikiModel.ts similarity index 75% rename from src/models/WikiModel.ts rename to packages/obsidian/src/models/WikiModel.ts index ee49c2dc..8216d158 100644 --- a/src/models/WikiModel.ts +++ b/packages/obsidian/src/models/WikiModel.ts @@ -1,7 +1,7 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { mediaDbTag, migrateObject } from 'packages/obsidian/src/utils/Utils'; export type WikiData = ModelToData; diff --git a/src/settings/Icon.tsx b/packages/obsidian/src/settings/Icon.tsx similarity index 100% rename from src/settings/Icon.tsx rename to packages/obsidian/src/settings/Icon.tsx diff --git a/src/settings/PropertyMapper.ts b/packages/obsidian/src/settings/PropertyMapper.ts similarity index 87% rename from src/settings/PropertyMapper.ts rename to packages/obsidian/src/settings/PropertyMapper.ts index b34f245d..831bb7d1 100644 --- a/src/settings/PropertyMapper.ts +++ b/packages/obsidian/src/settings/PropertyMapper.ts @@ -1,7 +1,8 @@ -import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { PropertyMappingOption } from './PropertyMapping'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { PropertyMappingOption } from 'packages/obsidian/src/settings/PropertyMapping'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; export class PropertyMapper { plugin: MediaDbPlugin; @@ -75,7 +76,7 @@ export class PropertyMapper { if (obj.type === 'manga') { obj.type = 'comicManga'; - console.debug(`MDB | updated metadata type`, obj.type); + Logger.debug(`MDB | updated metadata type`, obj.type); } if (!MEDIA_TYPES.includes(obj.type as MediaType)) { return obj; diff --git a/src/settings/PropertyMapping.ts b/packages/obsidian/src/settings/PropertyMapping.ts similarity index 96% rename from src/settings/PropertyMapping.ts rename to packages/obsidian/src/settings/PropertyMapping.ts index 82d044ea..62e86c8e 100644 --- a/src/settings/PropertyMapping.ts +++ b/packages/obsidian/src/settings/PropertyMapping.ts @@ -1,5 +1,6 @@ -import type { MediaType } from '../utils/MediaType'; -import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from 'packages/obsidian/src/utils/Utils'; // Plain object interfaces for serialization export interface PropertyMappingData { @@ -33,7 +34,7 @@ export class PropertyMappingModel { } validate(): { res: boolean; err?: Error } { - console.debug(`MDB | validated property mappings for ${this.type}`); + Logger.debug(`MDB | validated property mappings for ${this.type}`); // check properties for (const property of this.properties) { diff --git a/src/settings/PropertyMappingModelComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx similarity index 100% rename from src/settings/PropertyMappingModelComponent.tsx rename to packages/obsidian/src/settings/PropertyMappingModelComponent.tsx diff --git a/src/settings/PropertyMappingModelsComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx similarity index 100% rename from src/settings/PropertyMappingModelsComponent.tsx rename to packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx diff --git a/src/settings/Settings.ts b/packages/obsidian/src/settings/Settings.ts similarity index 95% rename from src/settings/Settings.ts rename to packages/obsidian/src/settings/Settings.ts index b1808884..40ceb8d6 100644 --- a/src/settings/Settings.ts +++ b/packages/obsidian/src/settings/Settings.ts @@ -10,6 +10,7 @@ import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderS import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; +import { render } from 'solid-js/web'; function createDateFormatDescription(preview: string): DocumentFragment { return createFragment(frag => { @@ -424,25 +425,14 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; - private propertyMappingModelsComponent?: PropertyMappingModelsComponent; constructor(app: App, plugin: MediaDbPlugin) { super(app, plugin); this.plugin = plugin; } - override hide(): void { - this.propertyMappingModelsComponent?.unload(); - this.propertyMappingModelsComponent = undefined; - super.hide(); - } - display(): void { const { containerEl } = this; - if (this.propertyMappingModelsComponent) { - this.propertyMappingModelsComponent.unload(); - this.propertyMappingModelsComponent = undefined; - } containerEl.empty(); const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); @@ -847,22 +837,25 @@ export class MediaDbSettingTab extends PluginSettingTab { mappingGroup.setHeading('Property mappings'); mappingGroup.addSetting(setting => { setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription()); - const propertyMappingsEl = setting.descEl.createDiv(); - this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, { - models: structuredClone(this.plugin.settings.propertyMappingModels), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }); - this.propertyMappingModelsComponent.load(); + + render( + () => + PropertyMappingModelsComponent({ + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }), + setting.descEl, + ); }); } } -} \ No newline at end of file +} diff --git a/src/settings/suggesters/FileSuggest.ts b/packages/obsidian/src/settings/suggesters/FileSuggest.ts similarity index 100% rename from src/settings/suggesters/FileSuggest.ts rename to packages/obsidian/src/settings/suggesters/FileSuggest.ts diff --git a/src/settings/suggesters/FolderSuggest.ts b/packages/obsidian/src/settings/suggesters/FolderSuggest.ts similarity index 100% rename from src/settings/suggesters/FolderSuggest.ts rename to packages/obsidian/src/settings/suggesters/FolderSuggest.ts diff --git a/src/styles.css b/packages/obsidian/src/styles.css similarity index 99% rename from src/styles.css rename to packages/obsidian/src/styles.css index afdfcc6f..444af288 100644 --- a/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -68,6 +68,10 @@ small.media-db-plugin-list-text { margin-bottom: 10px; } +.media-db-plugin-search-input { + width: 100%; +} + .media-db-plugin-button:focus { /*outline: 1px solid white;*/ } diff --git a/packages/obsidian/src/utils/AppError.ts b/packages/obsidian/src/utils/AppError.ts new file mode 100644 index 00000000..50db6841 --- /dev/null +++ b/packages/obsidian/src/utils/AppError.ts @@ -0,0 +1,24 @@ +export enum AppErrorKind { + Validation = 'Validation', + Api = 'Api', + Network = 'Network', + Vault = 'Vault', + Modal = 'Modal', + Cancelled = 'Cancelled', + Unexpected = 'Unexpected', +} + +export interface AppError { + kind: AppErrorKind; + message: string; + userMessage?: string; + cause?: unknown; + context?: Record; +} + +export const appError = (error: AppError): AppError => error; + +export const toAppError = (cause: unknown, fallback: Omit): AppError => { + const message = cause instanceof Error ? cause.message : String(cause); + return { ...fallback, message: fallback.message || message, cause }; +}; diff --git a/src/utils/BulkImportHelper.ts b/packages/obsidian/src/utils/BulkImportHelper.ts similarity index 67% rename from src/utils/BulkImportHelper.ts rename to packages/obsidian/src/utils/BulkImportHelper.ts index da4b1a42..f91ae374 100644 --- a/src/utils/BulkImportHelper.ts +++ b/packages/obsidian/src/utils/BulkImportHelper.ts @@ -1,10 +1,10 @@ import type { TFolder } from 'obsidian'; import { TFile } from 'obsidian'; -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'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; +import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils'; export enum BulkImportLookupMethod { ID = 'id', @@ -50,7 +50,7 @@ export class BulkImportHelper { continue; } - const metadata = this.plugin.getMetadataFromFileCache(file); + const metadata = this.plugin.fileHelper.getMetadataFromFileCache(file); const lookupValue = metadata[fieldName]; if (!lookupValue || typeof lookupValue !== 'string') { @@ -81,60 +81,54 @@ export class BulkImportHelper { } private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - try { - const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); - if (model) { - await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined); - return undefined; - } else { - return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; - } - } catch (e) { - return { filePath: file.path, error: `${e}` }; + const modelResult = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); + if (!modelResult.ok) { + return { filePath: file.path, error: modelResult.error.userMessage ?? modelResult.error.message }; + } + + if (modelResult.value) { + await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + return undefined; } + + return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; } private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - let results: MediaTypeModel[] = []; - try { - results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); - } catch (e) { - return { filePath: file.path, error: `${e}` }; + const resultsResult = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); + if (!resultsResult.ok) { + return { filePath: file.path, error: resultsResult.error.userMessage ?? resultsResult.error.message }; } + + const results: MediaTypeModel[] = resultsResult.value.items; if (!results || results.length === 0) { return { filePath: file.path, error: `no search results` }; } - const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({ + const selectModalResult = await this.plugin.modalHelper.createSelectModalOutcome({ elements: results, skipButton: true, modalTitle: `Results for '${lookupValue}'`, }); - if (selectModalResult.code === ModalResultCode.ERROR) { - selectModal.close(); - return { filePath: file.path, error: selectModalResult.error.message }; + if (selectModalResult.status === OutcomeStatus.Error) { + return { filePath: file.path, error: selectModalResult.error.userMessage ?? selectModalResult.error.message }; } - if (selectModalResult.code === ModalResultCode.CLOSE) { - selectModal.close(); + if (selectModalResult.status === OutcomeStatus.Cancelled) { return { filePath: file.path, error: 'user canceled', canceled: true }; } - if (selectModalResult.code === ModalResultCode.SKIP) { - selectModal.close(); + if (selectModalResult.status === OutcomeStatus.Skipped) { return { filePath: file.path, error: 'user skipped' }; } if (selectModalResult.data.selected.length === 0) { - selectModal.close(); return { filePath: file.path, error: `no search results selected` }; } - const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); - await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); - - selectModal.close(); + const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected); + await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); return undefined; } diff --git a/src/utils/DateFormatter.ts b/packages/obsidian/src/utils/DateFormatter.ts similarity index 80% rename from src/utils/DateFormatter.ts rename to packages/obsidian/src/utils/DateFormatter.ts index 31169e5b..bc120647 100644 --- a/src/utils/DateFormatter.ts +++ b/packages/obsidian/src/utils/DateFormatter.ts @@ -1,8 +1,6 @@ -import type momentType from 'moment'; -import type { Moment } from 'moment'; -import { moment as obsidianMoment } from 'obsidian'; +import { moment } from 'obsidian'; -const moment: typeof momentType = obsidianMoment as unknown as typeof momentType; +const momentFn = 'default' in moment ? moment.default : moment; export class DateFormatter { private static readonly RFC2822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; @@ -21,7 +19,7 @@ export class DateFormatter { } getPreview(format?: string): string { - const today = moment(); + const today = momentFn(); format ??= this.toFormat; @@ -44,32 +42,32 @@ export class DateFormatter { return null; } - let date: Moment; + let date: moment.Moment; if (!dateFormat) { date = this.parseWithoutFormat(dateString); } else { - date = moment(dateString, dateFormat, locale); + date = momentFn(dateString, dateFormat, locale); } // format date (if it is valid) return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null; } - private parseWithoutFormat(dateString: string): Moment { + private parseWithoutFormat(dateString: string): moment.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); + return momentFn(dateString); } // fall back to native Date parsing for unknown formats - return moment(new Date(dateString)); + return momentFn(new Date(dateString)); } private hasMomentFormat(dateString: string): boolean { - const date = moment(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode + const date = momentFn(dateString, [moment.ISO_8601, DateFormatter.RFC2822_FORMAT], true); // strict mode return date.isValid(); } } diff --git a/packages/obsidian/src/utils/ErrorReporter.ts b/packages/obsidian/src/utils/ErrorReporter.ts new file mode 100644 index 00000000..4155ae1a --- /dev/null +++ b/packages/obsidian/src/utils/ErrorReporter.ts @@ -0,0 +1,23 @@ +import { Notice } from 'obsidian'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; + +export class ErrorReporter { + notice(error: AppError): void { + const message = error.userMessage ?? error.message; + new Notice(message); + } + + log(error: AppError): void { + Logger.warn('MDB | error', error); + } + + report(error: AppError): void { + this.log(error); + + if (error.kind !== AppErrorKind.Cancelled) { + this.notice(error); + } + } +} diff --git a/src/utils/IconList.ts b/packages/obsidian/src/utils/IconList.ts similarity index 100% rename from src/utils/IconList.ts rename to packages/obsidian/src/utils/IconList.ts diff --git a/src/utils/IllegalFilenameCharactersList.ts b/packages/obsidian/src/utils/IllegalFilenameCharactersList.ts similarity index 100% rename from src/utils/IllegalFilenameCharactersList.ts rename to packages/obsidian/src/utils/IllegalFilenameCharactersList.ts diff --git a/packages/obsidian/src/utils/Logger.ts b/packages/obsidian/src/utils/Logger.ts new file mode 100644 index 00000000..ab88308d --- /dev/null +++ b/packages/obsidian/src/utils/Logger.ts @@ -0,0 +1,38 @@ +declare const __LOG_LEVEL__: number; + +enum LogLevel { + NONE = 0, + ERROR = 1, + WARN = 2, + LOG = 3, + DEBUG = 4, +} + +const logLevel: LogLevel = typeof __LOG_LEVEL__ !== 'undefined' ? __LOG_LEVEL__ : LogLevel.LOG; + +export class Logger { + static debug(...args: unknown[]): void { + if (logLevel >= LogLevel.DEBUG) { + console.debug(...args); + } + } + + static log(...args: unknown[]): void { + if (logLevel >= LogLevel.LOG) { + // eslint-disable-next-line obsidianmd/rule-custom-message -- These log statements are disabled for production builds. + console.log(...args); + } + } + + static warn(...args: unknown[]): void { + if (logLevel >= LogLevel.WARN) { + console.warn(...args); + } + } + + static error(...args: unknown[]): void { + if (logLevel >= LogLevel.ERROR) { + console.error(...args); + } + } +} diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts new file mode 100644 index 00000000..222a11c0 --- /dev/null +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -0,0 +1,289 @@ +import { MarkdownView, Notice } from 'obsidian'; +import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; + +export class MediaDbEntryHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + private reportAppError(error: AppError): void { + this.plugin.errorReporter.report(error); + } + + async createLinkWithSearchModal(): Promise { + const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + if (!advancedSearch) { + return; + } + + const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const selectResults = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items, multiSelect: false }); + if (!selectResults || selectResults.selected.length < 1) { + return; + } + + const detailedResults = await this.queryDetails(selectResults.selected); + if (detailedResults.length < 1) { + return; + } + + const link = `[${detailedResults[0].title}](${detailedResults[0].url})`; + const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView); + + if (view) { + view.editor.replaceRange(link, view.editor.getCursor()); + } + } + + async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { + const searchData = await this.plugin.modalHelper.promptSearchModal(searchModalOptions ?? {}); + if (!searchData) { + return; + } + + const types = searchData.types; + const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); + const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const filteredSearchResults = apiSearchResults.value.items.filter(result => types.includes(result.getMediaType())); + if (filteredSearchResults.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); + return; + } + + const selectResultsData = + types.length === 1 && types[0] === MediaType.Season + ? await this.plugin.modalHelper.promptSelectModal({ + elements: filteredSearchResults, + description: 'Select one search result to proceed.', + submitButtonText: 'Ok', + }) + : await this.plugin.modalHelper.promptSelectModal({ elements: filteredSearchResults }); + + if (!selectResultsData) { + return; + } + + const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected); + + if (selectResults.length === 0) { + return; + } + + const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); + if (seasonHandlingResult.handled) { + return; + } + + const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults }); + if (!confirmed?.confirmed) { + return; + } + + await this.plugin.fileHelper.createMediaDbNotes(selectResults); + } + + async createEntryWithAdvancedSearchModal(): Promise { + const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + if (!advancedSearch) { + return; + } + + const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + if (!apiSearchResults.ok) { + this.reportAppError(apiSearchResults.error); + return; + } + + if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { + this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + return; + } + + const selectResultsData = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items }); + if (!selectResultsData) { + return; + } + + const selectResults = await this.queryDetails(selectResultsData.selected); + if (selectResults.length < 1) { + return; + } + + const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults }); + if (!confirmed?.confirmed) { + return; + } + + await this.plugin.fileHelper.createMediaDbNotes(selectResults); + } + + async createEntryWithIdSearchModal(): Promise { + let idSearchResult: MediaTypeModel | undefined = undefined; + let proceed = false; + + while (!proceed) { + const idSearchData = await this.plugin.modalHelper.promptIdSearchModal({}); + if (!idSearchData) { + return; + } + + const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); + if (!queriedIdResult.ok) { + this.reportAppError(queriedIdResult.error); + return; + } + + idSearchResult = queriedIdResult.value; + if (!idSearchResult) { + return; + } + + const previewData = await this.plugin.modalHelper.promptPreviewModal({ elements: [idSearchResult] }); + if (!previewData) { + return; + } + + proceed = previewData.confirmed; + } + + if (!idSearchResult) { + return; + } + + const createNoteResult = await this.plugin.fileHelper.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); + if (!createNoteResult.ok) { + this.reportAppError(createNoteResult.error); + } + } + + async queryDetails(models: MediaTypeModel[]): Promise { + const results = await Promise.all(models.map(model => this.plugin.apiManager.queryDetailedInfo(model))); + + const detailModels: MediaTypeModel[] = []; + for (const result of results) { + if (result.ok && result.value) { + detailModels.push(result.value); + } else if (!result.ok) { + this.reportAppError(result.error); + } + } + + return detailModels; + } + + private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { + if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { + const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); + return { handled: true, seasonsCreated: created }; + } + + if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) { + const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI'); + if (seriesResults.length === 1 && types.includes('season')) { + const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); + return { handled: true, seasonsCreated: created }; + } + } + + return { handled: false }; + } + + private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { + const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined; + if (!tmdbSeasonAPI) { + new Notice('TMDBSeasonAPI not available.'); + return false; + } + + const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); + if (!allSeasonsResult.ok) { + this.reportAppError(allSeasonsResult.error); + new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); + return false; + } + + const allSeasons = allSeasonsResult.value; + if (allSeasons.length === 0) { + new Notice('No seasons found for this series.'); + return false; + } + + const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); + if (!selectedSeasons || selectedSeasons.length === 0) { + return false; + } + + await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); + new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); + return true; + } + + private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { + const modal = new MediaDbSeasonSelectModal( + this.plugin, + allSeasons.map(season => ({ + season_number: season.seasonNumber, + name: season.seasonTitle || season.title, + episode_count: season.episodes || 0, + air_date: season.year, + poster_path: season.image, + })), + true, + seriesTitle, + ); + + return await new Promise(resolve => { + modal.setSubmitCb(resolve); + modal.open(); + }); + } + + private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { + await Promise.all( + selectedSeasons.map(async selectedSeason => { + const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number); + if (seasonModel) { + const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); + if (!fullMetadataResult.ok) { + this.reportAppError(fullMetadataResult.error); + new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); + return; + } + + await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]); + } + }), + ); + } +} diff --git a/packages/obsidian/src/utils/MediaDbFileHelper.ts b/packages/obsidian/src/utils/MediaDbFileHelper.ts new file mode 100644 index 00000000..42f1bdd4 --- /dev/null +++ b/packages/obsidian/src/utils/MediaDbFileHelper.ts @@ -0,0 +1,325 @@ +import type { TFile } from 'obsidian'; +import { TFolder } from 'obsidian'; +import { Notice, normalizePath, parseYaml, requestUrl, stringifyYaml } from 'obsidian'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { ConfirmOverwriteModal } from 'packages/obsidian/src/modals/ConfirmOverwriteModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Result } from 'packages/obsidian/src/utils/result'; +import { err, ok } from 'packages/obsidian/src/utils/result'; +import type { CreateNoteOptions } from 'packages/obsidian/src/utils/Utils'; +import { hasTemplaterPlugin, replaceIllegalFileNameCharactersInString, useTemplaterPluginInFile } from 'packages/obsidian/src/utils/Utils'; + +export type Metadata = Record; + +export interface MediaTypeModelObj { + id: string; + type: MediaType; + dataSource: string; +} + +export class MediaDbFileHelper { + readonly plugin: MediaDbPlugin; + private readonly frontMatterRegExpPattern = '^(---)\\n[\\s\\S]*?\\n---'; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { + const results = await Promise.all(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile }))); + + const failures = results.filter(result => !result.ok); + if (failures.length > 0) { + Logger.warn( + 'MDB | Some notes failed to create:', + failures.map(result => result.error), + ); + new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); + return err(failures[0].error); + } + + return ok(undefined); + } + + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { + Logger.debug('MDB | creating new note'); + + options.openNote = this.plugin.settings.openNoteInNewTab; + + if (this.plugin.settings.imageDownload) { + const imageResult = await this.downloadImageForMediaModel(mediaTypeModel); + if (!imageResult.ok) { + return imageResult; + } + } + + const fileContentResult = await this.attempt(() => this.generateMediaDbNoteContents(mediaTypeModel, options), { + kind: AppErrorKind.Unexpected, + message: 'Failed to generate note contents', + userMessage: 'Failed to generate note contents', + }); + if (!fileContentResult.ok) { + return fileContentResult; + } + + const folderResult = await this.attempt(() => this.plugin.mediaTypeManager.getFolder(mediaTypeModel, this.plugin.app), { + kind: AppErrorKind.Vault, + message: 'Failed to determine note folder', + userMessage: 'Failed to determine note folder', + }); + if (!folderResult.ok) { + return folderResult; + } + + options.folder ??= folderResult.value; + + const targetFileResult = await this.createNote(this.plugin.mediaTypeManager.getFileName(mediaTypeModel), fileContentResult.value, options); + if (!targetFileResult.ok) { + return targetFileResult; + } + + if (this.plugin.settings.enableTemplaterIntegration) { + const templaterResult = await this.attempt(() => useTemplaterPluginInFile(this.plugin.app, targetFileResult.value), { + kind: AppErrorKind.Unexpected, + message: 'Failed to apply templater to the note', + userMessage: 'Failed to apply templater to the note', + }); + if (!templaterResult.ok) { + return templaterResult; + } + } + + return ok(undefined); + } + + async updateActiveNote(onlyMetadata: boolean = false): Promise { + const activeFile = this.plugin.app.workspace.getActiveFile() ?? undefined; + if (!activeFile) { + throw new Error('MDB | there is no active note'); + } + + let metadata = this.getMetadataFromFileCache(activeFile); + metadata = this.plugin.modelPropertyMapper.convertObjectBack(metadata); + + Logger.debug(`MDB | read metadata`, metadata); + + if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { + throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); + } + + const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; + Logger.debug(`MDB | validOldMetadata`, validOldMetadata); + + const oldMediaTypeModel = this.plugin.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); + Logger.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); + + const newMediaTypeModelResult = await this.plugin.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); + if (!newMediaTypeModelResult.ok) { + this.plugin.errorReporter.report(newMediaTypeModelResult.error); + return; + } + + let newMediaTypeModel = newMediaTypeModelResult.value; + if (!newMediaTypeModel) { + return; + } + + newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); + Logger.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); + + if (onlyMetadata) { + const result = await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); + if (!result.ok) { + this.plugin.errorReporter.report(result.error); + } + } else { + const result = await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); + if (!result.ok) { + this.plugin.errorReporter.report(result.error); + } + } + } + + generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { + const fileMetadata = this.plugin.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + return stringifyYaml(fileMetadata); + } + + async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + let template = await this.plugin.mediaTypeManager.getTemplate(mediaTypeModel, this.plugin.app); + let fileMetadata: Record; + + if (this.plugin.settings.useDefaultFrontMatter) { + fileMetadata = this.plugin.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + } else { + fileMetadata = { + id: mediaTypeModel.id, + type: mediaTypeModel.type, + dataSource: mediaTypeModel.dataSource, + }; + } + + let fileContent = ''; + template = options.attachTemplate ? template : ''; + + ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); + ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); + + if (this.plugin.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.plugin.app)) { + fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } else { + fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } + + return fileContent; + } + + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!fileToAttach) { + return { fileMetadata, fileContent }; + } + + const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); + fileMetadata = { ...attachFileMetadata, ...fileMetadata }; + + let attachFileContent = await this.plugin.app.vault.read(fileToAttach); + const regExp = new RegExp(this.frontMatterRegExpPattern); + attachFileContent = attachFileContent.replace(regExp, ''); + attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; + fileContent += attachFileContent; + + return { fileMetadata, fileContent }; + } + + async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!template) { + return { fileMetadata, fileContent }; + } + + const templateMetadata = this.getMetaDataFromFileContent(template); + fileMetadata = { ...templateMetadata, ...fileMetadata }; + + const regExp = new RegExp(this.frontMatterRegExpPattern); + const attachFileContent = template.replace(regExp, ''); + fileContent += attachFileContent; + + return { fileMetadata, fileContent }; + } + + getMetaDataFromFileContent(fileContent: string): Metadata { + const regExp = new RegExp(this.frontMatterRegExpPattern); + const frontMatterRegExpResult = regExp.exec(fileContent); + if (!frontMatterRegExpResult) { + return {}; + } + + let frontMatter = frontMatterRegExpResult[0]; + if (!frontMatter) { + return {}; + } + + frontMatter = frontMatter.substring(4); + frontMatter = frontMatter.substring(0, frontMatter.length - 3); + + let metadata = parseYaml(frontMatter) as Metadata; + if (!metadata) { + metadata = {}; + } + + Logger.debug(`MDB | metadata read from file content`, metadata); + + return metadata; + } + + getMetadataFromFileCache(file: TFile): Metadata { + const metadata: Metadata | undefined = this.plugin.app.metadataCache.getFileCache(file)?.frontmatter; + return structuredClone(metadata ?? {}); + } + + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { + const folder = options.folder ?? this.plugin.app.vault.getAbstractFileByPath('/'); + + if (!folder || !(folder instanceof TFolder)) { + return err({ kind: AppErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); + } + + fileName = replaceIllegalFileNameCharactersInString(fileName); + const filePath = `${folder.path}/${fileName}.md`; + + const file = this.plugin.app.vault.getAbstractFileByPath(filePath); + if (file) { + const shouldOverwrite = await new Promise(resolve => { + new ConfirmOverwriteModal(this.plugin.app, fileName, resolve).open(); + }); + + if (!shouldOverwrite) { + return err({ kind: AppErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); + } + + await this.plugin.app.fileManager.trashFile(file); + } + + const targetFile = await this.plugin.app.vault.create(filePath, fileContent); + Logger.debug(`MDB | created new file at ${filePath}`); + + if (options.openNote) { + const activeLeaf = this.plugin.app.workspace.getLeaf(false); + if (!activeLeaf) { + Logger.warn('MDB | no active leaf, not opening newly created note'); + return ok(targetFile); + } + await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); + } + + return ok(targetFile); + } + + private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { + if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { + const imageUrl = mediaTypeModel.image; + const imageResult = await this.attempt( + async () => { + const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; + const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; + const imagePath = normalizePath(`${this.plugin.settings.imageFolder}/${imageFileName}`); + + if (!this.plugin.app.vault.getAbstractFileByPath(this.plugin.settings.imageFolder)) { + await this.plugin.app.vault.createFolder(this.plugin.settings.imageFolder); + } + + if (!this.plugin.app.vault.getAbstractFileByPath(imagePath)) { + const response = await requestUrl({ url: imageUrl, method: 'GET' }); + await this.plugin.app.vault.createBinary(imagePath, response.arrayBuffer); + } + + mediaTypeModel.image = `[[${imagePath}]]`; + }, + { + kind: AppErrorKind.Network, + message: 'MDB | Failed to download image', + userMessage: 'Failed to download image', + }, + ); + + if (!imageResult.ok) { + Logger.warn('MDB | Failed to download image:', imageResult.error); + return imageResult; + } + } + + return ok(undefined); + } + + private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { + return await Promise.resolve() + .then(operation) + .then( + value => ok(value), + cause => err(toAppError(cause, fallback)), + ); + } +} diff --git a/src/utils/MediaType.ts b/packages/obsidian/src/utils/MediaType.ts similarity index 100% rename from src/utils/MediaType.ts rename to packages/obsidian/src/utils/MediaType.ts diff --git a/src/utils/MediaTypeManager.ts b/packages/obsidian/src/utils/MediaTypeManager.ts similarity index 82% rename from src/utils/MediaTypeManager.ts rename to packages/obsidian/src/utils/MediaTypeManager.ts index d9685734..f3ef631b 100644 --- a/src/utils/MediaTypeManager.ts +++ b/packages/obsidian/src/utils/MediaTypeManager.ts @@ -1,19 +1,20 @@ -import type { App, TFile } from 'obsidian'; +import type { App } from 'obsidian'; +import { TFile } from 'obsidian'; import { TFolder } from 'obsidian'; -import { BoardGameModel } from '../models/BoardGameModel'; -import { BookModel } from '../models/BookModel'; -import { ComicMangaModel } from '../models/ComicMangaModel'; -import { GameModel } from '../models/GameModel'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MovieModel } from '../models/MovieModel'; -import { MusicReleaseModel } from '../models/MusicReleaseModel'; -import { SeasonModel } from '../models/SeasonModel'; -import { SeriesModel } from '../models/SeriesModel'; -import { WikiModel } from '../models/WikiModel'; -import type { MediaDbPluginSettings } from '../settings/Settings'; -import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; -import { MediaType } from './MediaType'; -import { replaceTags } from './Utils'; +import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; +import { BookModel } from 'packages/obsidian/src/models/BookModel'; +import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; +import { GameModel } from 'packages/obsidian/src/models/GameModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; +import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; +import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; +import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; +import type { MediaDbPluginSettings } from 'packages/obsidian/src/settings/Settings'; +import { ILLEGAL_FILENAME_CHARACTERS } from 'packages/obsidian/src/utils/IllegalFilenameCharactersList'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import { replaceTags } from 'packages/obsidian/src/utils/Utils'; // All media types in alphabetical order export const MEDIA_TYPES: MediaType[] = [ @@ -110,16 +111,16 @@ export class MediaTypeManager { } } - const template = await app.vault.cachedRead(templateFile as TFile); - // console.log(template); + if (!(templateFile instanceof TFile)) { + return ''; + } + + const template = await app.vault.cachedRead(templateFile); return replaceTags(template, mediaTypeModel); } async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise { - let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); - - folderPath ??= `/`; - // console.log(folderPath); + let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()) ?? '/'; if (!(await app.vault.adapter.exists(folderPath))) { await app.vault.createFolder(folderPath); diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts new file mode 100644 index 00000000..89a1b244 --- /dev/null +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -0,0 +1,282 @@ +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaDbAdvancedSearchModal } from 'packages/obsidian/src/modals/MediaDbAdvancedSearchModal'; +import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSearchModal'; +import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; +import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; +import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { Outcome } from 'packages/obsidian/src/utils/result'; +import { cancelled, failure, OutcomeStatus, skipped, success } from 'packages/obsidian/src/utils/result'; + +export interface SearchModalData { + query: string; + types: MediaType[]; +} + +export interface AdvancedSearchModalData { + query: string; + apis: string[]; +} + +export interface IdSearchModalData { + query: string; + api: string; +} + +export interface SelectModalData { + selected: MediaTypeModel[]; +} + +export interface PreviewModalData { + confirmed: boolean; +} + +export interface SearchModalOptions { + modalTitle?: string; + preselectedTypes?: MediaType[]; + prefilledSearchString?: string; +} + +export interface AdvancedSearchModalOptions { + modalTitle?: string; + preselectedAPIs?: string[]; + prefilledSearchString?: string; +} + +export interface IdSearchModalOptions { + modalTitle?: string; + preselectedAPI?: string; + prefilledSearchString?: string; +} + +export interface SelectModalOptions { + elements?: MediaTypeModel[]; + multiSelect?: boolean; + modalTitle?: string; + skipButton?: boolean; + description?: string; + submitButtonText?: string; +} + +export interface PreviewModalOptions { + modalTitle?: string; + elements?: MediaTypeModel[]; +} + +export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { + modalTitle: 'Media DB Search', + preselectedTypes: [], + prefilledSearchString: '', +}; + +export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { + modalTitle: 'Media DB Advanced Search', + preselectedAPIs: [], + prefilledSearchString: '', +}; + +export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { + modalTitle: 'Media DB Id Search', + preselectedAPI: undefined, + prefilledSearchString: '', +}; + +export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { + modalTitle: 'Media DB Search Results', + elements: [], + multiSelect: true, + skipButton: false, +}; + +export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { + modalTitle: 'Media DB Preview', + elements: [], +}; + +export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { + elements: [], + multiSelect: true, + modalTitle: '', + skipButton: false, + description: 'Select one or multiple search results.', + submitButtonText: 'Ok', +}; + +interface ModalCoreResult { + modalResult: Outcome; + modal: TModal; +} + +export class ModalHelper { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + private async openModalCore( + createModal: () => TModal, + wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, + ): Promise> { + const modal = createModal(); + const modalResult = await new Promise>(resolve => { + wireHandlers(modal, resolve); + modal.open(); + }); + + return { modalResult, modal }; + } + + private async resolveOutcome(outcomePromise: Promise>): Promise { + const outcome = await outcomePromise; + + if (outcome.status === OutcomeStatus.Ok) { + return outcome.data; + } + + if (outcome.status === OutcomeStatus.Error) { + this.plugin.errorReporter.report(outcome.error); + } + + return undefined; + } + + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbSearchModal(this.plugin, searchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptSearchModal(searchModalOptions: SearchModalOptions): Promise { + return await this.resolveOutcome(this.createSearchModalOutcome(searchModalOptions)); + } + + async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise { + return await this.resolveOutcome(this.createAdvancedSearchModalOutcome(advancedSearchModalOptions)); + } + + async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Id search modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise { + return await this.resolveOutcome(this.createIdSearchModalOutcome(idSearchModalOptions)); + } + + async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setSkipCallback(() => resolve(skipped())); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Select modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { + modal.close(); + } + + return modalResult; + } + + async promptSelectModal(selectModalOptions: SelectModalOptions): Promise { + return await this.resolveOutcome(this.createSelectModalOutcome(selectModalOptions)); + } + + async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { + const { modalResult, modal } = await this.openModalCore( + () => new MediaDbPreviewModal(this.plugin, previewModalOptions), + (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => { + if (err) { + resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Preview modal closed with an error' }))); + return; + } + + resolve(cancelled()); + }); + }, + ); + + if (modalResult.status === OutcomeStatus.Ok) { + modal.close(); + } + + return modalResult; + } + + async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise { + return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); + } +} diff --git a/src/utils/Utils.ts b/packages/obsidian/src/utils/Utils.ts similarity index 96% rename from src/utils/Utils.ts rename to packages/obsidian/src/utils/Utils.ts index 9e4f286c..cc5b2bd9 100644 --- a/src/utils/Utils.ts +++ b/packages/obsidian/src/utils/Utils.ts @@ -1,13 +1,12 @@ import { iso6392 } from 'iso-639-2'; import type { TFile, TFolder, App } from 'obsidian'; import { requestUrl } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; -export const mediaDbVersion: string = '0.5.2'; -export const debug: boolean = true; +export const mediaDbVersion: string = '0.8.0'; export function wrapAround(value: number, size: number): number { if (size <= 0) { @@ -153,10 +152,6 @@ export function markdownTable(content: string[][]): string { return table; } -export function fragWithHTML(html: string): DocumentFragment { - return createFragment(frag => (frag.createDiv().innerHTML = html)); -} - export function dateToString(date: Date): string { return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; } @@ -240,7 +235,7 @@ function getTemplaterPlugin(app: App): TemplaterPlugin | null { return null; } - return candidate as TemplaterPlugin; + return candidate; } export function hasTemplaterPlugin(app: App): boolean { diff --git a/packages/obsidian/src/utils/result.ts b/packages/obsidian/src/utils/result.ts new file mode 100644 index 00000000..64c71c35 --- /dev/null +++ b/packages/obsidian/src/utils/result.ts @@ -0,0 +1,52 @@ +export interface Ok { + ok: true; + value: T; +} +export interface Err { + ok: false; + error: E; +} +export type Result = Ok | Err; + +export const ok = (value: T): Ok => ({ ok: true, value }); +export const err = (error: E): Err => ({ ok: false, error }); + +export const isOk = (result: Result): result is Ok => result.ok; +export const isErr = (result: Result): result is Err => !result.ok; + +export const mapResult = (result: Result, mapper: (value: T) => U): Result => (result.ok ? ok(mapper(result.value)) : result); + +export const mapError = (result: Result, mapper: (error: E) => F): Result => (result.ok ? result : err(mapper(result.error))); + +export const andThen = (result: Result, binder: (value: T) => Result): Result => (result.ok ? binder(result.value) : result); + +export const tapError = (result: Result, sideEffect: (error: E) => void): Result => { + if (!result.ok) sideEffect(result.error); + return result; +}; + +export const fromPromise = async (promise: Promise, onError: (cause: unknown) => E): Promise> => { + try { + return ok(await promise); + } catch (cause) { + return err(onError(cause)); + } +}; + +export enum OutcomeStatus { + Ok = 'ok', + Cancelled = 'cancelled', + Skipped = 'skipped', + Error = 'error', +} + +export type Outcome = + | { status: OutcomeStatus.Ok; data: T } + | { status: OutcomeStatus.Cancelled } + | { status: OutcomeStatus.Skipped } + | { status: OutcomeStatus.Error; error: E }; + +export const cancelled = (): Outcome => ({ status: OutcomeStatus.Cancelled }); +export const skipped = (): Outcome => ({ status: OutcomeStatus.Skipped }); +export const success = (data: T): Outcome => ({ status: OutcomeStatus.Ok, data }); +export const failure = (error: E): Outcome => ({ status: OutcomeStatus.Error, error }); diff --git a/src/api/schemas/GiantBomb.json b/packages/schemas/src/GiantBomb.json similarity index 100% rename from src/api/schemas/GiantBomb.json rename to packages/schemas/src/GiantBomb.json diff --git a/src/api/schemas/GiantBomb.ts b/packages/schemas/src/GiantBomb.ts similarity index 100% rename from src/api/schemas/GiantBomb.ts rename to packages/schemas/src/GiantBomb.ts diff --git a/src/api/schemas/MALAPI.ts b/packages/schemas/src/MALAPI.ts similarity index 100% rename from src/api/schemas/MALAPI.ts rename to packages/schemas/src/MALAPI.ts diff --git a/src/api/schemas/OpenLibrary.json b/packages/schemas/src/OpenLibrary.json similarity index 100% rename from src/api/schemas/OpenLibrary.json rename to packages/schemas/src/OpenLibrary.json diff --git a/src/api/schemas/OpenLibrary.ts b/packages/schemas/src/OpenLibrary.ts similarity index 100% rename from src/api/schemas/OpenLibrary.ts rename to packages/schemas/src/OpenLibrary.ts diff --git a/src/api/schemas/TMDB.ts b/packages/schemas/src/TMDB.ts similarity index 100% rename from src/api/schemas/TMDB.ts rename to packages/schemas/src/TMDB.ts diff --git a/packages/schemas/src/fetchSchemas.sh b/packages/schemas/src/fetchSchemas.sh new file mode 100644 index 00000000..e5793eed --- /dev/null +++ b/packages/schemas/src/fetchSchemas.sh @@ -0,0 +1,15 @@ +#! /bin/env bash + +# IMPORTANT: needs to be ran from this directory, otherwise the output files will be generated in the wrong place + +# https://docs.api.jikan.moe/ +bun openapi-typescript https://raw.githubusercontent.com/jikan-me/jikan-rest/master/storage/api-docs/api-docs.json -o ./MALAPI.ts + +# https://www.giantbomb.com/forums/api-developers-3017/giant-bomb-openapi-specification-1901269/ +bun openapi-typescript ./GiantBomb.json -o ./GiantBomb.ts + +# https://github.com/internetarchive/openlibrary-api/blob/main/swagger.yaml +bun openapi-typescript ./OpenLibrary.json -o ./OpenLibrary.ts + +# https://developer.themoviedb.org/openapi +bun openapi-typescript https://developer.themoviedb.org/openapi/tmdb-api.json -o ./TMDB.ts \ No newline at end of file diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts deleted file mode 100644 index 00dca1a5..00000000 --- a/src/api/APIManager.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Notice } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { APIModel } from './APIModel'; - -export class APIManager { - apis: APIModel[]; - - constructor() { - this.apis = []; - } - - /** - * Queries the basic info for one query string and multiple APIs. - * - * @param query - * @param apisToQuery - */ - async query(query: string, apisToQuery: string[]): Promise { - console.debug(`MDB | api manager queried with "${query}"`); - - const promises = this.apis - .filter(api => apisToQuery.includes(api.apiName)) - .map(async api => { - try { - return await api.searchByTitle(query); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return []; - } - }); - - return (await Promise.all(promises)).flat(); - } - - /** - * Queries detailed information for a MediaTypeModel. - * - * @param item - */ - async queryDetailedInfo(item: MediaTypeModel): Promise { - return await this.queryDetailedInfoById(item.id, item.dataSource); - } - - /** - * Queries detailed info for an id from an API. - * - * @param id - * @param apiName - */ - async queryDetailedInfoById(id: string, apiName: string): Promise { - for (const api of this.apis) { - if (api.apiName === apiName) { - try { - return await api.getById(id); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return undefined; - } - } - } - - return undefined; - } - - getApiByName(name: string): APIModel | undefined { - for (const api of this.apis) { - if (api.apiName === name) { - return api; - } - } - - return undefined; - } - - registerAPI(api: APIModel): void { - this.apis.push(api); - } -} diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts deleted file mode 100644 index bddddb76..00000000 --- a/src/api/apis/BoardGameGeekAPI.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { requestUrl } from 'obsidian'; -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'; - -// sadly no open api schema available - -export class BoardGameGeekAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'BoardGameGeekAPI'; - this.apiDescription = 'A free API for BoardGameGeek things.'; - this.apiUrl = 'https://boardgamegeek.com/xmlapi/'; - this.types = [MediaType.BoardGame]; - } - - 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 ${key}`, - }, - }); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - - // console.debug(response); - - const ret: MediaTypeModel[] = []; - - for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { - const id = boardgame.attributes.getNamedItem('objectid')?.value; - const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined; - const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; - - ret.push( - new BoardGameModel({ - dataSource: this.apiName, - id, - title, - englishTitle: title, - year, - }), - ); - } - - return ret; - } - - 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 ${key}`, - }, - }); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - // console.debug(response); - - const boardgame = response.querySelector('boardgame'); - if (!boardgame) { - throw Error(`MDB | Received invalid data from ${this.apiName}.`); - } - - const title = boardgame.querySelector('name[primary=true]')?.textContent; - const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; - const image = boardgame.querySelector('image')?.textContent ?? undefined; - const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); - const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')) - .map(n => n.textContent) - .filter(n => n !== null); - const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); - const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); - const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); - const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; - const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')) - .map(n => n.textContent) - .filter(n => n !== null); - - return new BoardGameModel({ - title: title ?? undefined, - englishTitle: title ?? undefined, - year: year === '0' ? '' : year, - dataSource: this.apiName, - url: `https://boardgamegeek.com/boardgame/${id}`, - id: id, - - genres: genres, - onlineRating: onlineRating, - complexityRating: complexityRating, - minPlayers: minPlayers, - maxPlayers: maxPlayers, - playtime: playtime, - publishers: publishers, - image: image, - - released: true, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts deleted file mode 100644 index da7e7adc..00000000 --- a/src/api/apis/ComicVineAPI.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - -import { requestUrl } from 'obsidian'; -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'; - -// sadly no open api schema available - -export class ComicVineAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'ComicVineAPI'; - this.apiDescription = 'A free API for comic books.'; - this.apiUrl = 'https://comicvine.gamespot.com/api'; - this.types = [MediaType.ComicManga]; - } - - 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=${key}&format=json&resources=volume&query=${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - // console.debug(fetchData); - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const ret: MediaTypeModel[] = []; - for (const result of data.results) { - ret.push( - new ComicMangaModel({ - title: result.name, - englishTitle: result.name, - year: result.start_year, - dataSource: this.apiName, - id: `4050-${result.id}`, - publishers: result.publisher?.name, - }), - ); - } - - return ret; - } - - 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=${key}&format=json`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - const result = data.results; - - const authors = result.people as - | { - name: string; - }[] - | undefined; - - return new ComicMangaModel({ - type: MediaType.ComicManga, - title: result.name, - englishTitle: result.name, - alternateTitles: result.aliases, - plot: result.deck, - year: result.start_year, - dataSource: this.apiName, - url: result.site_detail_url, - id: `4050-${result.id}`, - - authors: authors?.map(x => x.name), - chapters: result.count_of_issues, - image: result.image?.original_url, - - released: true, - publishers: result.publisher?.name, - publishedFrom: result.start_year, - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.ComicVineAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts deleted file mode 100644 index 6d6d1ea4..00000000 --- a/src/api/apis/GiantBombAPI.ts +++ /dev/null @@ -1,167 +0,0 @@ -import createClient from 'openapi-fetch'; -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'; - -export class GiantBombAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'GiantBombAPI'; - this.apiDescription = 'A free API for games.'; - this.apiUrl = 'https://www.giantbomb.com/api'; - this.types = [MediaType.Game]; - } - - 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 (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/games', { - params: { - query: { - api_key: key, - filter: `name:${title}`, - format: 'json', - limit: 20, - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.results; - - const ret: MediaTypeModel[] = []; - for (const result of data ?? []) { - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; - - ret.push( - new GameModel({ - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - id: result.guid?.toString(), - }), - ); - } - - return ret; - } - - 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 (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/game/{guid}', { - params: { - path: { - guid: id, - }, - query: { - api_key: key, - format: 'json', - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.results; - - if (!result) { - throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); - } - - console.log(result); - - // sadly the only OpenAPI definition I could find doesn't have the right types - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; - const developers = result.developers as - | { - name: string; - }[] - | undefined; - const publishers = result.publishers as - | { - name: string; - }[] - | undefined; - const genres = result.genres as - | { - name: string; - }[] - | undefined; - const image = result.image as - | { - small_url: string; - medium_url: string; - super_url: string; - } - | undefined; - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - url: result.site_detail_url, - id: result.guid?.toString(), - developers: developers?.map(x => x.name), - publishers: publishers?.map(x => x.name), - genres: genres?.map(x => x.name), - onlineRating: 0, - image: image?.super_url, - - released: true, - releaseDate: result.original_release_date, - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.GiantBombAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts deleted file mode 100644 index 14094d0b..00000000 --- a/src/api/apis/IGDBAPI.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface IGDBCover { - url: string; -} - -interface IGDBGenre { - name: string; -} - -interface IGDBCompany { - name: string; -} - -interface IGDBInvolvedCompany { - company: IGDBCompany; - developer: boolean; - publisher: boolean; -} - -interface IGDBGame { - id: number; - name: string; - cover?: IGDBCover; - first_release_date?: number; - summary?: string; - total_rating?: number; - url?: string; - genres?: IGDBGenre[]; - involved_companies?: IGDBInvolvedCompany[]; -} - -interface TwitchAuthResponse { - access_token: string; - expires_in: number; -} - -export class IGDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - private accessToken: string = ''; - private tokenExpiry: number = 0; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'IGDBAPI'; - this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; - this.apiUrl = 'https://api.igdb.com/v4'; - this.types = [MediaType.Game]; - } - - private async getAuthToken(): Promise { - const currentTime = Date.now(); - if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - const clientSecret = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientSecret); - - if (!clientId || !clientSecret) { - throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); - } - console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); - const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, - method: 'POST', - }); - if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); - const data = response.json as TwitchAuthResponse; - this.accessToken = data.access_token; - this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; - return this.accessToken; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); - const token = await this.getAuthToken(); - const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - return data.map(result => { - const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: year, - dataSource: this.apiName, - id: result.id.toString(), - image: image, - }); - }); - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); - if (!clientId) throw Error(`MDB | Client ID for ${this.apiName} missing.`); - const token = await this.getAuthToken(); - const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, - method: 'POST', - headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); - const result = data[0]; - - const developers: string[] = []; - const publishers: string[] = []; - result.involved_companies?.forEach(c => { - if (c.developer) developers.push(c.company.name); - if (c.publisher) publishers.push(c.company.name); - }); - const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.url, - id: result.id.toString(), - developers: developers, - publishers: publishers, - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.total_rating, - image: image, - released: true, - releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', - userData: { played: false, personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.IGDBAPI_disabledMediaTypes ?? []; - } -} diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts deleted file mode 100644 index 3083e234..00000000 --- a/src/api/apis/MALAPI.ts +++ /dev/null @@ -1,229 +0,0 @@ -import createClient from 'openapi-fetch'; -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'; - -export class MALAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MALAPI'; - this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; - this.apiUrl = 'https://jikan.moe/'; - this.types = [MediaType.Movie, MediaType.Series]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('special', 'special'); - this.typeMappings.set('tv', 'series'); - this.typeMappings.set('ova', 'ova'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime', { - params: { - query: { - q: title, - limit: 20, - sfw: this.plugin.settings.sfwFilter ? true : false, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.data; - - const ret: MediaTypeModel[] = []; - - for (const result of data ?? []) { - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString() ?? ''; - const id = result.mal_id?.toString(); - - if (type === undefined) { - ret.push( - new MovieModel({ - subType: '', - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - if (type === 'movie' || type === 'special') { - ret.push( - new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } else if (type === 'series' || type === 'ova') { - ret.push( - new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime/{id}/full', { - params: { - path: { - id: id as unknown as number, // This is fine - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.data; - - if (result === undefined) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); - } - - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString(); - const new_id = result.mal_id?.toString(); - - if (type === undefined) { - return new MovieModel({ - subType: undefined, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - if (type === 'movie' || type === 'special') { - return new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series' || type === 'ova') { - return new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - episodes: result.episodes, - duration: result.duration, - onlineRating: result.score, - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), - airing: result.airing, - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MALAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts deleted file mode 100644 index a8972b3b..00000000 --- a/src/api/apis/MobyGamesAPI.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ - -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -// TODO: maybe we should remove this API, as it can no longer be tested without paying for an API key - -export class MobyGamesAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-DD-MM'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MobyGamesAPI'; - this.apiDescription = 'A free API for games.'; - this.apiUrl = 'https://api.mobygames.com/v1'; - this.types = [MediaType.Game]; - } - - 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 (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${key}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - // console.debug(fetchData); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (fetchData.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const ret: MediaTypeModel[] = []; - for (const result of data.games) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), - dataSource: this.apiName, - id: result.game_id, - }), - ); - } - - return ret; - } - - 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 (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${key}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const result = data.games[0]; - - return new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), - dataSource: this.apiName, - url: `https://www.mobygames.com/game/${result.game_id}`, - id: result.game_id, - developers: [], - publishers: [], - genres: result.genres?.map((x: any) => x.genre_name) ?? [], - onlineRating: result.moby_score, - image: result.sample_cover?.image ?? '', - - released: true, - releaseDate: result.platforms[0].first_release_date ?? 'unknown', - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MobyGamesAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts deleted file mode 100644 index e1d0d7a8..00000000 --- a/src/api/apis/MusicBrainzAPI.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MusicReleaseModel } from '../../models/MusicReleaseModel'; -import { MediaType } from '../../utils/MediaType'; -import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -interface Tag { - name: string; - count: number; -} -interface Genre { - name: string; - count: number; - id: string; - disambiguation: string; -} -interface Release { - id: string; - 'status-id': string; - title: string; - status: string; -} - -interface ArtistCredit { - name: string; - artist: { - tags: Tag[]; - type: string; - id: string; - name: string; - 'short-name': string; - country: string; - }; -} - -interface SearchResponse { - id: string; - 'type-id': string; - score: number; - 'primary-type-id': string; - 'artists-credit-id': string; - count: number; - title: string; - 'first-release-date': string; - 'primary-type': string; - 'artist-credit': ArtistCredit[]; - releases: Release[]; - tags: Tag[]; -} - -interface IdResponse { - id: string; - tags: Tag[]; - 'primary-type-id': string; - 'artist-credit': ArtistCredit[]; - title: string; - genres: Genre[]; - 'first-release-date': string; - releases: Release[]; - 'primary-type': string; - rating: { - value: number; - 'votes-count': number; - }; -} - -interface MediaResponse { - media: { - 'track-count': number; - tracks: { - 'artist-credit': ArtistCredit[]; - length: number | null; - number: string; - position: number; - title: string; - recording: { - length: number; - title: string; - }; - }[]; - }[]; - 'text-representation': { - language: string; - script: string; - }; -} - -export class MusicBrainzAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MusicBrainz API'; - this.apiDescription = 'Free API for music albums.'; - this.apiUrl = 'https://musicbrainz.org/'; - this.types = [MediaType.MusicRelease]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; - - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json) as { - 'release-groups': SearchResponse[]; - }; - // console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data['release-groups']) { - ret.push( - new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - subType: result['primary-type'], - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - // Fetch release group - const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; - const groupResponse = await requestUrl({ - url: groupUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (groupResponse.status !== 200) { - throw Error(`MDB | Received status code ${groupResponse.status} from ${this.apiName}.`); - } - - const result = (await groupResponse.json) as IdResponse; - - // Get ID of the first release - const firstRelease = result.releases?.[0]; - if (!firstRelease) { - throw Error('MDB | No releases found in release group.'); - } - - // Fetch recordings for the first release - const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; - console.log(`MDB | Fetching release recordings from: ${releaseUrl}`); - - const releaseResponse = await requestUrl({ - url: releaseUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (releaseResponse.status !== 200) { - throw Error(`MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`); - } - - const releaseData = (await releaseResponse.json) as MediaResponse; - const tracks = extractTracksFromMedia(releaseData.media); - - // Calculate total album length for the first release - const totalrawLength = - releaseData.media[0]?.tracks.reduce((sum, track) => { - const len = track.length ?? track.recording?.length; - return typeof len === 'number' && !isNaN(len) ? sum + len : sum; - }, 0) ?? 0; - const albumLengthCalc = millisecondsToMinutes(totalrawLength); - - console.log(releaseData); - - return new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', - genres: result.genres.map(g => g.name), - subType: result['primary-type'], - albumDuration: albumLengthCalc, - trackCount: releaseData.media[0]?.['track-count'] ?? 0, - tracks: tracks, - rating: result.rating.value * 2, - - userData: { - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; - } -} - -function extractTracksFromMedia(media: MediaResponse['media']): { - number: number; - title: string; - duration: string; - featuredArtists: string[]; -}[] { - if (!media || media.length === 0 || !media[0].tracks) return []; - - return media[0].tracks.map((track, index) => { - const title = track.title ?? track.recording?.title ?? 'Unknown Title'; - const rawLength = track.length ?? track.recording?.length; - const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; - const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; - - return { - number: index + 1, - title, - duration, - featuredArtists, - }; - }); -} - -function millisecondsToMinutes(milliseconds: number): string { - const minutes = Math.floor(milliseconds / 60000); - const seconds = Math.floor((milliseconds % 60000) / 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}`; -} diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts deleted file mode 100644 index 38620807..00000000 --- a/src/api/apis/OMDbAPI.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { SeriesModel } from '../../models/SeriesModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface ErrorResponse { - Response: 'False'; - Error: string; -} - -type SearchResponse = - | { - Response: 'True'; - totalResults: string; - Search: { - Title: string; - Year: string; - Poster: string; - imdbID: string; - Type: string; - }[]; - } - | ErrorResponse; - -type IdResponse = - | { - Response: 'True'; - Title: string; - Year: string; - Rated: string; - Released: string; - Runtime: string; - Genre: string; - Director: string; - Writer: string; - Actors: string; - Plot: string; - Language: string; - Country: string; - Awards: string; - Poster: string; - Metascore: string; - imdbRating: string; - imdbVotes: string; - imdbID: string; - Type: string; - DVD: string; - BoxOffice: string; - Production: string; - Website: string; - } - | ErrorResponse; - -export class OMDbAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'DD MMM YYYY'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OMDbAPI'; - this.apiDescription = 'A free API for Movies, Series and Games.'; - this.apiUrl = 'https://www.omdbapi.com/'; - this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('series', 'series'); - this.typeMappings.set('game', 'game'); - } - - 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 (!key) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${key}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const data = response.json as SearchResponse | undefined; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.Response === 'False') { - if (data.Error === 'Movie not found!') { - return []; - } - - throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`); - } - if (!data.Search) { - return []; - } - - // console.debug(data.Search); - - const ret: MediaTypeModel[] = []; - - for (const result of data.Search) { - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - continue; - } - if (type === 'movie') { - ret.push( - new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'series') { - ret.push( - new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'game') { - ret.push( - new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } - } - - return ret; - } - - 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 (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${key}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const result = response.json as IdResponse | undefined; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (result.Response === 'False') { - throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); - } - - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - throw Error(`${result.Type.toLowerCase()} is an unsupported type.`); - } - - if (type === 'movie') { - return new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - director: result.Director?.split(', '), - writer: result.Writer?.split(', '), - duration: result.Runtime, - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - boxOffice: result.BoxOffice, - ageRating: result.Rated, - premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series') { - return new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - writer: result.Writer?.split(', '), - studio: [], - episodes: 0, - duration: result.Runtime, - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - ageRating: result.Rated, - airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'game') { - return new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: result.Year, - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - genres: result.Genre?.split(', '), - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OMDbAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts deleted file mode 100644 index f42f7918..00000000 --- a/src/api/apis/OpenLibraryAPI.ts +++ /dev/null @@ -1,161 +0,0 @@ -import createClient from 'openapi-fetch'; -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'; - -interface SearchResponse { - editions: { - docs: { - key?: string; - title?: string; - cover_i?: number; - isbn?: string[]; - }[]; - }; - cover_i?: number; - has_fulltext?: boolean; - edition_count?: number; - title?: string; - author_name?: string[]; - first_publish_year?: number; - key: string; - description?: string; - - number_of_pages_median?: number; - isbn?: string[]; - ratings_average?: number; -} - -export class OpenLibraryAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OpenLibraryAPI'; - this.apiDescription = 'A free API for books'; - this.apiUrl = 'https://openlibrary.org/'; - this.types = [MediaType.Book]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: title, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - }; - - // console.debug(data); - - const ret: MediaTypeModel[] = []; - - for (const result of data.docs) { - ret.push( - new BookModel({ - title: result.title, - englishTitle: result.title, - year: result.first_publish_year?.toString() ?? 'unknown', - dataSource: this.apiName, - id: result.key, - author: result.author_name?.join(', '), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - q?: string; - }; - - const result = data.docs[0]; - - let key = result.key; - let title = result.title; - let cover_i = result.cover_i; - let isbnArr = result.isbn; - - // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present - const q = data.q ?? ''; - if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { - const edition = result.editions.docs[0]; - key = edition.key ?? key; - title = edition.title ?? title; - cover_i = edition.cover_i ?? cover_i; - isbnArr = edition.isbn ?? isbnArr; - } - - const pages = Number(result.number_of_pages_median); - const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); - const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); - - return new BookModel({ - title: title, - year: result.first_publish_year?.toString() ?? 'unknown', - dataSource: this.apiName, - url: `https://openlibrary.org` + key, - id: key, - isbn: Number.isNaN(isbn) ? undefined : isbn, - isbn13: Number.isNaN(isbn13) ? undefined : isbn13, - englishTitle: title, - - author: result.author_name?.join(', '), - plot: result.description ?? undefined, - pages: Number.isNaN(pages) ? undefined : pages, - onlineRating: result.ratings_average, - image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, - - released: true, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts deleted file mode 100644 index ec0a53ea..00000000 --- a/src/api/apis/RAWGAPI.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface RAWGGame { - id: number; - name: string; - released?: string; - background_image?: string; - name_original?: string; - website?: string; - slug?: string; - metacritic?: number; - developers?: { name: string }[]; - publishers?: { name: string }[]; - genres?: { name: string }[]; -} - -interface RAWGSearchResponse { - results: RAWGGame[]; -} - -export class RAWGAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'RAWGAPI'; - this.apiDescription = 'A large open video game database.'; - this.apiUrl = 'https://api.rawg.io/api'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${key}&search=${encodeURIComponent(title)}&page_size=20`, - method: 'GET', - }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } - - const data = response.json as RAWGSearchResponse; - return data.results.map( - result => - new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - id: result.id.toString(), - image: result.background_image, - }), - ); - } - - async getById(id: string): Promise { - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); - if (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${key}`, - method: 'GET', - }); - if (response.status !== 200) { - throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - } - - const result = response.json as RAWGGame; - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name_original ?? result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', - dataSource: this.apiName, - url: result.website ?? `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), - developers: result.developers?.map(d => d.name) ?? [], - publishers: result.publishers?.map(p => p.name) ?? [], - genres: result.genres?.map(g => g.name) ?? [], - onlineRating: result.metacritic, - image: result.background_image, - released: result.released != null, - releaseDate: result.released, - userData: { played: false, personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.RAWGAPI_disabledMediaTypes ?? []; - } -} diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts deleted file mode 100644 index 4f4e1741..00000000 --- a/src/api/apis/TMDBMovieAPI.ts +++ /dev/null @@ -1,187 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -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; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBMovieAPI'; - this.apiDescription = 'A community built Movie DB.'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Movie]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - } - - 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 (!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 ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !data.results) { - return []; - } - - // console.debug(data.results); - - const ret: MediaTypeModel[] = []; - - for (const result of data.results) { - ret.push( - new MovieModel({ - type: 'movie', - title: result.original_title, - englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); - - 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 ${key}`, - }, - params: { - path: { movie_id: parseInt(id) }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // console.debug(result); - const credits = (result as TMDBCreditsResponse).credits; - - return new MovieModel({ - type: 'movie', - title: result.title, - englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', - premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, - url: `https://www.themoviedb.org/movie/${result.id}`, - id: result.id.toString(), - - plot: result.overview ?? '', - genres: result.genres?.map(g => 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, - actors: getTopCastNames(credits, 5), - image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, - - released: ['Released'].includes(result.status!), - streamingServices: [], - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts deleted file mode 100644 index 09258031..00000000 --- a/src/api/apis/TMDBSeasonAPI.ts +++ /dev/null @@ -1,304 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { SeasonModel } from '../../models/SeasonModel'; -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; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBSeasonAPI'; - this.apiDescription = 'A community built Series DB (seasons).'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Season]; - this.typeMappings = new Map(); - this.typeMappings.set('tv', 'season'); - } - - 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 (!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 ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (searchResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (searchResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`); - } - - const searchData = searchResponse.data; - - if (!searchData?.results || searchData.total_results === 0) { - return []; - } - - const topResults = searchData.results.slice(0, 20); - - 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, - }); - - if (detailsResponse.response.status === 200 && Array.isArray(detailsResponse.data?.seasons)) { - totalSeasons = detailsResponse.data.seasons.length; - } - } catch { - // Ignore detail errors and use 0 as fallback. - } - } - - 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', - dataSource: this.apiName, - id: result.id?.toString() ?? '', - seasonTitle: result.name ?? result.original_name ?? '', - seasonNumber: totalSeasons, - image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', - }); - }), - ); - } - - // Fetch all seasons for a given series - async getSeasonsForSeries(tvId: string): Promise { - 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 ${key}`, - }, - params: { - path: { series_id: Number.parseInt(tvId, 10) }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - const seriesName = seriesData?.name ?? ''; - - const ret: SeasonModel[] = []; - - if (Array.isArray(seriesData?.seasons)) { - for (const season of seriesData.seasons) { - const seasonNumber = season.season_number ?? 0; - const titleText = `${seriesName} - Season ${seasonNumber}`; - - ret.push( - new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: `${tvId}/season/${seasonNumber}`, - seasonTitle: season.name ?? titleText, - seasonNumber: seasonNumber, - image: season.poster_path ?? '', - }), - ); - } - } - - return ret; - } - - 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 (!key) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - // Expect season ids like "12345/season/2" - const m = /^(\d+)\/season\/(\d+)$/.exec(id); - if (!m) { - throw Error(`MDB | Invalid season id "${id}". Expected format "/season/".`); - } - - 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 ${key}`, - }, - params: { - path: { - series_id: tvId, - season_number: seasonNumber, - }, - }, - fetch: fetch, - }); - - if (seasonResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seasonResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`); - } - - const seasonData = seasonResponse.data; - if (!seasonData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // Fetch parent series to build consistent titles and inherit fields - const seriesResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${key}`, - }, - params: { - path: { series_id: tvId }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - - if (!seriesData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - const seriesName = seriesData?.name ?? ''; - const airDate = seasonData.air_date ?? ''; - const titleText = `${seriesName} - Season ${seasonData.season_number}`; - - // Get airedTo as the air_date of the last episode, if available - let airedTo = 'unknown'; - if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { - const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; - if (lastEp?.air_date) airedTo = lastEp.air_date; - } - 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.toString()}/season/${seasonData.season_number}`, - id: `${tvId.toString()}/season/${seasonData.season_number}`, - seasonTitle: seasonData.name ?? titleText, - seasonNumber: seasonData.season_number ?? seasonNumber, - episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, - airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', - airedTo: formattedAiredTo, - plot: seasonData.overview ?? '', - image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', - 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, - actors: getTopActorNames((seriesData as { credits?: CreditsLike }).credits), - released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), - streamingServices: [], - airing: ['Returning Series'].includes(seriesData.status ?? ''), - userData: { watched: false, lastWatched: '', personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return []; - } -} diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts deleted file mode 100644 index cb6ce28c..00000000 --- a/src/api/apis/TMDBSeriesAPI.ts +++ /dev/null @@ -1,179 +0,0 @@ -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { SeriesModel } from '../../models/SeriesModel'; -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; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TMDBSeriesAPI'; - this.apiDescription = 'A community built Series DB.'; - this.apiUrl = 'https://www.themoviedb.org/'; - this.types = [MediaType.Series]; - this.typeMappings = new Map(); - this.typeMappings.set('tv', 'series'); - } - - 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 (!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 ${key}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !data.results) { - return []; - } - - // console.debug(data.results); - - const ret: MediaTypeModel[] = []; - - for (const result of data.results) { - ret.push( - new SeriesModel({ - type: 'series', - title: result.original_name, - englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - 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 (!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 ${key}`, - }, - params: { - path: { series_id: parseInt(id) }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // console.debug(result); - const credits = (result as TMDBCreditsResponse).credits; - - return new SeriesModel({ - type: 'series', - title: result.original_name, - englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - url: `https://www.themoviedb.org/tv/${result.id}`, - id: result.id.toString(), - - plot: result.overview ?? '', - 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, - 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!), - streamingServices: [], - airing: ['Returning Series'].includes(result.status!), - airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown', - airedTo: ['Returning Series'].includes(result.status!) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts deleted file mode 100644 index 0139d4ed..00000000 --- a/src/api/apis/VNDBAPI.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -enum VNDevStatus { - Finished, - InDevelopment, - Cancelled, -} - -enum TagSpoiler { - None, - Minor, - Major, -} - -enum TagCategory { - Content = 'cont', - Sexual = 'ero', - Technical = 'tech', -} - -/** - * A partial `POST /vn` response payload; desired fields should be listed in the request body. - */ -interface VNJSONResponse { - more: boolean; - results: [ - { - id: string; - title: string; - titles: [ - { - title: string; - lang: string; - }, - ]; - devstatus: VNDevStatus; - released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents - image: { - url: string; - sexual: number; - } | null; - rating: number | null; - tags: [ - { - id: string; - name: string; - category: TagCategory; - rating: number; - spoiler: TagSpoiler; - }, - ]; - developers: [ - { - id: string; - name: string; - }, - ]; - }, - ]; -} - -/** - * A partial `POST /release` response payload; desired fields should be listed in the request body. - */ -interface ReleaseJSONResponse { - more: boolean; - results: [ - { - id: string; - producers: [ - { - id: string; - name: string; - developer: boolean; - publisher: boolean; - }, - ]; - }, - ]; -} - -export class VNDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'VNDB API'; - this.apiDescription = 'A free API for visual novels.'; - this.apiUrl = 'https://api.vndb.org/kana'; - this.types = [MediaType.Game]; - } - - /** - * Make a `POST` request to the VNDB API. - * @param endpoint The API endpoint to query. E.g. "/vn". - * @param body A JSON object defining the query, following the VNDB API structure. - * @returns A JSON object representing the query response. - * @throws Error The request returned an unsuccessful or unexpected HTTP status code. - * @see {@link https://api.vndb.org/kana#api-structure} - */ - private async postQuery(endpoint: string, body: string): Promise { - const fetchData = await requestUrl({ - url: `${this.apiUrl}${endpoint}`, - method: 'POST', - contentType: 'application/json', - body: body, - throw: false, - }); - - if (fetchData.status !== 200) { - switch (fetchData.status) { - case 400: - throw Error(`MDB | Invalid request body or query [${fetchData.text}].`); - case 404: - throw Error(`MDB | Invalid API path or HTTP method.`); - case 429: - throw Error(`MDB | Throttled.`); - case 500: - throw Error(`MDB | VNDB server error.`); - case 502: - throw Error(`MDB | VNDB server is down.`); - default: - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - } - - return fetchData.json; - } - - /** - * Make a `POST` request to the `/vn` endpoint. - * Queries visual novel entries. - * @see {@link https://api.vndb.org/kana#post-vn} - */ - private postVNQuery(body: string): Promise { - return this.postQuery('/vn', body) as Promise; - } - - /** - * Make a `POST` request to the `/release` endpoint. - * Queries release entries. - * @see {@link https://api.vndb.org/kana#post-release} - */ - private postReleaseQuery(body: string): Promise { - return this.postQuery('/release', body) as Promise; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - /* SFW Filter: has ANY official&&complete&&standalone&&SFW release - OR has NO official&&standalone&&NSFW release - OR has the `In-game Sexual Content Toggle` (g2708) tag */ - // prettier-ignore - const vnData = await this.postVNQuery(`{ - "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : - `, ["or" - , ["release", "=", ["and" - , ["official", "=", "1"] - , ["rtype", "=", "complete"] - , ["patch", "!=", "1"] - , ["has_ero", "!=", "1"] - ]] - , ["release", "!=", ["and" - , ["official", "=", "1"] - , ["patch", "!=", "1"] - , ["has_ero", "=", "1"] - ]] - , ["tag", "=", "g2708"] - ]`} - , ["search", "=", "${title}"] - ], - "fields": "title, titles{title, lang}, released", - "sort": "searchrank", - "results": 20 - }`); - - const ret: MediaTypeModel[] = []; - for (const vn of vnData.results) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', - dataSource: this.apiName, - id: vn.id, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const vnData = await this.postVNQuery(`{ - "filters": ["id", "=", "${id}"], - "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" - }`); - - if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); - const vn = vnData.results[0]; - const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; - vn.released ??= 'Unknown'; - - const releaseData = await this.postReleaseQuery(`{ - "filters": ["and" - , ["vn", "=" - , ["id", "=", "${id}"] - ] - , ["official", "=", 1] - ], - "fields": "producers.name, producers.publisher, producers.developer", - "results": 100 - }`); - - return new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, - dataSource: this.apiName, - url: `https://vndb.org/${vn.id}`, - id: vn.id, - - developers: vn.developers.map(d => d.name), - publishers: releaseData.results - .flatMap(r => r.producers) - .filter(p => p.publisher) - .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list - .map(p => p.name) - .unique(), - genres: vn.tags - .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) - .sort((t1, t2) => t2.rating - t1.rating) - .map(t => t.name), - onlineRating: vn.rating ?? NaN, - // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively - image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, - - released: vn.devstatus === VNDevStatus.Finished, - releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.VNDBAPI_disabledMediaTypes; - } -} diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts deleted file mode 100644 index 9b3b2896..00000000 --- a/src/api/apis/WikipediaAPI.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { WikiModel } from '../../models/WikiModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface SearchResponse { - query: { - search: { - title: string; - pageid: number; - }[]; - }; -} - -interface IdResponse { - query: { - pages: Record; - }; -} - -interface WikipediaPage { - pageid: number; - title: string; - contentmodel: string; - pagelanguage: string; - pagelanguagehtmlcode: string; - pagelanguagedir: string; - touched: string; // ISO date string - lastrevid: number; - length: number; - fullurl: string; - editurl: string; - canonicalurl: string; -} -export class WikipediaAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'Wikipedia API'; - this.apiDescription = 'The API behind Wikipedia'; - this.apiUrl = 'https://www.wikipedia.com'; - this.types = [MediaType.Wiki]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as SearchResponse; - console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data.query.search) { - ret.push( - new WikiModel({ - type: 'wiki', - title: result.title, - englishTitle: result.title, - year: '', - dataSource: this.apiName, - id: result.pageid.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as IdResponse; - // console.debug(data); - const result = Object.values(data?.query?.pages)[0]; - - return new WikiModel({ - title: result.title, - englishTitle: result.title, - dataSource: this.apiName, - url: result.fullurl, - id: result.pageid.toString(), - - wikiUrl: result.fullurl, - lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), - length: result.length, - - userData: {}, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.WikipediaAPI_disabledMediaTypes; - } -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 8a83e92c..00000000 --- a/src/main.ts +++ /dev/null @@ -1,743 +0,0 @@ -import type { TFile } from 'obsidian'; -import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian'; -import { requestUrl, normalizePath } from 'obsidian'; -import { APIManager } from './api/APIManager'; -import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; -import { ComicVineAPI } from './api/apis/ComicVineAPI'; -import { GiantBombAPI } from './api/apis/GiantBombAPI'; -import { IGDBAPI } from './api/apis/IGDBAPI'; -import { MALAPI } from './api/apis/MALAPI'; -import { MALAPIManga } from './api/apis/MALAPIManga'; -import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; -import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; -import { OMDbAPI } from './api/apis/OMDbAPI'; -import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; -import { SteamAPI } from './api/apis/SteamAPI'; -import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; -import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; -import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; -import { VNDBAPI } from './api/apis/VNDBAPI'; -import { WikipediaAPI } from './api/apis/WikipediaAPI'; -import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; -import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; -import type { MediaTypeModel } from './models/MediaTypeModel'; -import type { SeasonModel } from './models/SeasonModel'; -import { PropertyMapper } from './settings/PropertyMapper'; -import { PropertyMappingModel } from './settings/PropertyMapping'; -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'; -import type { CreateNoteOptions } from './utils/Utils'; -import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; -import 'src/styles.css'; - -export type Metadata = Record; - -export interface MediaTypeModelObj { - id: string; - type: MediaType; - dataSource: string; -} - -export default class MediaDbPlugin extends Plugin { - settings!: MediaDbPluginSettings; - apiManager!: APIManager; - mediaTypeManager!: MediaTypeManager; - modelPropertyMapper!: PropertyMapper; - modalHelper!: ModalHelper; - bulkImportHelper!: BulkImportHelper; - dateFormatter!: DateFormatter; - - frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; - - async onload(): Promise { - this.apiManager = new APIManager(); - // register APIs - this.apiManager.registerAPI(new OMDbAPI(this)); - this.apiManager.registerAPI(new MALAPI(this)); - this.apiManager.registerAPI(new MALAPIManga(this)); - this.apiManager.registerAPI(new WikipediaAPI(this)); - this.apiManager.registerAPI(new MusicBrainzAPI(this)); - this.apiManager.registerAPI(new SteamAPI(this)); - this.apiManager.registerAPI(new TMDBSeriesAPI(this)); - this.apiManager.registerAPI(new TMDBSeasonAPI(this)); - this.apiManager.registerAPI(new TMDBMovieAPI(this)); - this.apiManager.registerAPI(new BoardGameGeekAPI(this)); - this.apiManager.registerAPI(new OpenLibraryAPI(this)); - this.apiManager.registerAPI(new ComicVineAPI(this)); - this.apiManager.registerAPI(new MobyGamesAPI(this)); - this.apiManager.registerAPI(new GiantBombAPI(this)); - this.apiManager.registerAPI(new IGDBAPI(this)); - this.apiManager.registerAPI(new RAWGAPI(this)); - this.apiManager.registerAPI(new VNDBAPI(this)); - - this.mediaTypeManager = new MediaTypeManager(); - this.modelPropertyMapper = new PropertyMapper(this); - this.modalHelper = new ModalHelper(this); - this.bulkImportHelper = new BulkImportHelper(this); - this.dateFormatter = new DateFormatter(); - - await this.loadSettings(); - // register the settings tab - this.addSettingTab(new MediaDbSettingTab(this.app, this)); - - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - // add icon to the left ribbon - const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.createEntryWithAdvancedSearchModal()); - ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); - - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file) => { - if (file instanceof TFolder) { - menu.addItem(item => { - item.setTitle('Import folder as Media DB entries') - .setIcon('database') - .onClick(() => this.bulkImportHelper.import(file)); - }); - } - }), - ); - - // register command to open search modal - this.addCommand({ - id: 'open-media-db-search-modal', - name: 'Create Media DB entry', - callback: () => this.createEntryWithSearchModal(), - }); - for (const mediaType of MEDIA_TYPES) { - this.addCommand({ - id: `open-media-db-search-modal-with-${mediaType}`, - name: `Create Media DB entry (${unCamelCase(mediaType)})`, - callback: () => this.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), - }); - } - this.addCommand({ - id: 'open-media-db-advanced-search-modal', - name: 'Create Media DB entry (advanced search)', - callback: () => this.createEntryWithAdvancedSearchModal(), - }); - // register command to open id search modal - this.addCommand({ - id: 'open-media-db-id-search-modal', - name: 'Create Media DB entry by id', - callback: () => this.createEntryWithIdSearchModal(), - }); - // register command to update the open note - this.addCommand({ - id: 'update-media-db-note', - name: 'Update open note (this will recreate the note)', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(false); - } - return true; - }, - }); - this.addCommand({ - id: 'update-media-db-note-metadata', - name: 'Update metadata', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(true); - } - return true; - }, - }); - // register link insert command - this.addCommand({ - id: 'add-media-db-link', - name: 'Insert link', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.createLinkWithSearchModal(); - } - return true; - }, - }); - } - - async createLinkWithSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults) { - return; - } - - const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }); - - if (!selectResults || selectResults.length < 1) { - return; - } - - const link = `[${selectResults[0].title}](${selectResults[0].url})`; - - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - - // Make sure the user is editing a Markdown file. - if (view) { - view.editor.replaceRange(link, view.editor.getCursor()); - } - } - - async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { - let types: string[] = []; - let apiSearchResults = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { - types = searchModalData.types; - const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); - try { - return await this.apiManager.query(searchModalData.query, apis); - } catch (e) { - console.warn('MDB | Query failed:', e); - new Notice(`Search failed: ${e}`); - return []; - } - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - // filter the results - apiSearchResults = apiSearchResults.filter(x => types.includes(x.type)); - - if (apiSearchResults.length === 0) { - new Notice('No results found for the selected types.'); - return; - } - - // Show selection modal - for seasons, skip detail query - const selectResults = - types.length === 1 && types[0] === 'season' - ? await this.modalHelper.openSelectModal( - { - elements: apiSearchResults, - description: 'Select one search result to proceed.', - submitButtonText: 'Ok', - }, - async selectModalData => selectModalData.selected, - ) - : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected)); - - if (!selectResults || selectResults.length === 0) { - return; - } - - // Handle season selection for both direct season searches and series-to-season conversion - const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); - if (seasonHandlingResult.handled) { - return; - } - - // Show preview and confirm - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => previewModalData.confirmed); - if (!confirmed) { - return; - } - - // User confirmed, create notes and exit - await this.createMediaDbNotes(selectResults); - } - - /** - * Handles the season workflow for both direct season searches and series-to-season conversion. - * Returns an object indicating what happened and how to proceed. - */ - private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { - // Case 1: User searched specifically for seasons and selected a series from TMDB - if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { - const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); - return { handled: true, seasonsCreated: created }; - } - - // Case 2: User searched for series but it's actually from TMDBSeasonAPI - // (This happens when searching for seasons returns series results) - if (types.includes('series') && selectResults.some(r => r.dataSource === 'TMDBSeriesAPI')) { - const seriesResults = selectResults.filter(r => r.dataSource === 'TMDBSeriesAPI'); - // If only one series result and user searched for seasons, show season selection - if (seriesResults.length === 1 && types.includes('season')) { - const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); - return { handled: true, seasonsCreated: created }; - } - } - - return { handled: false }; - } - - /** - * Shows the season selection modal for a given series and creates notes for selected seasons. - * Returns true if seasons were successfully created, false if cancelled. - */ - private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { - const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not available.'); - return false; - } - - try { - // Fetch all seasons for the selected series - const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); - if (!allSeasons || allSeasons.length === 0) { - new Notice('No seasons found for this series.'); - return false; - } - - // Show season selection modal - const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); - if (!selectedSeasons || selectedSeasons.length === 0) { - return false; - } - - // Create notes for all selected seasons in parallel - await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); - new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); - return true; - } catch (e) { - console.warn('MDB | Error in season selection workflow:', e); - new Notice(`Error loading seasons: ${e}`); - return false; - } - } - - /** - * Shows the season selection modal and returns the selected seasons. - */ - private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this, - allSeasons.map(s => ({ - season_number: s.seasonNumber, - name: s.seasonTitle || s.title, - episode_count: s.episodes || 0, - air_date: s.year, - poster_path: s.image, - })), - true, - seriesTitle, - ); - - return new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); - }); - } - - /** - * Creates notes for all selected seasons by fetching full metadata and creating entries. - */ - private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { - await Promise.all( - selectedSeasons.map(async selectedSeason => { - const seasonModel = allSeasons.find(s => s.seasonNumber === selectedSeason.season_number); - if (seasonModel) { - try { - // Fetch full metadata using getById - const fullMetadata = await tmdbSeasonAPI.getById(seasonModel.id); - await this.createMediaDbNotes([fullMetadata]); - } catch (e) { - console.warn(`MDB | Failed to create season ${selectedSeason.season_number}:`, e); - new Notice(`Failed to create season ${selectedSeason.season_number}: ${e}`); - } - } - }), - ); - } - - async createEntryWithAdvancedSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - const selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; - if (selectResults.length < 1) { - return; - } - - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - - await this.createMediaDbNotes(selectResults); - } - - async createEntryWithIdSearchModal(): Promise { - let idSearchResult: MediaTypeModel | undefined = undefined; - let proceed: boolean = false; - - while (!proceed) { - idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => { - return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api); - }); - if (!idSearchResult) { - return; - } - - proceed = await this.modalHelper.openPreviewModal({ elements: [idSearchResult] }, async previewModalData => { - return previewModalData.confirmed; - }); - } - - if (!idSearchResult) { - return; - } - await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); - } - - async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - // Create notes in parallel for better performance - const results = await Promise.allSettled(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }))); - - // Report any failures - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some notes failed to create:', failures); - new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); - } - } - - async queryDetails(models: MediaTypeModel[]): Promise { - // Query details in parallel for better performance - const results = await Promise.allSettled(models.map(model => this.apiManager.queryDetailedInfo(model))); - - // Filter out failures and return successful results - const detailModels: MediaTypeModel[] = results - .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled' && r.value !== undefined) - .map(r => r.value!); - - // Log failures for debugging - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some detail queries failed:', failures); - } - - return detailModels; - } - - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - try { - console.debug('MDB | creating new note'); - - options.openNote = this.settings.openNoteInNewTab; - - if (this.settings.imageDownload) { - await this.downloadImageForMediaModel(mediaTypeModel); - } - - const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); - - options.folder ??= await this.mediaTypeManager.getFolder(mediaTypeModel, this.app); - - const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); - - if (this.settings.enableTemplaterIntegration) { - await useTemplaterPluginInFile(this.app, targetFile); - } - } catch (e) { - console.warn(e); - new Notice(`${e}`); - } - } - - /** - * Tries to download the image for a media model. - * - * @param mediaTypeModel - * @returns true if the image was downloaded, false otherwise - */ - private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise { - if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { - try { - const imageUrl = mediaTypeModel.image; - const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; - const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; - const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`); - - if (!this.app.vault.getAbstractFileByPath(this.settings.imageFolder)) { - await this.app.vault.createFolder(this.settings.imageFolder); - } - - if (!this.app.vault.getAbstractFileByPath(imagePath)) { - const response = await requestUrl({ url: imageUrl, method: 'GET' }); - await this.app.vault.createBinary(imagePath, response.arrayBuffer); - } - - // Update model to use local image path - mediaTypeModel.image = `[[${imagePath}]]`; - return true; - } catch (e) { - console.warn('MDB | Failed to download image:', e); - } - } - - return false; - } - - generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { - const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - return stringifyYaml(fileMetadata); - } - - /** - * Generates the content of a note from a media model and some options. - * - * @param mediaTypeModel - * @param options - */ - async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - let fileMetadata: Record; - - if (this.settings.useDefaultFrontMatter) { - fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - } else { - fileMetadata = { - id: mediaTypeModel.id, - type: mediaTypeModel.type, - dataSource: mediaTypeModel.dataSource, - }; - } - - let fileContent = ''; - template = options.attachTemplate ? template : ''; - - ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); - ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); - - if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { - // Include the media variable in all templater commands by using a top level JavaScript execution command. - fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } else { - fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } - - return fileContent; - } - - async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!fileToAttach) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); - fileMetadata = { ...attachFileMetadata, ...fileMetadata }; - - let attachFileContent: string = await this.app.vault.read(fileToAttach); - const regExp = new RegExp(this.frontMatterRexExpPattern); - attachFileContent = attachFileContent.replace(regExp, ''); - attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!template) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const templateMetadata = this.getMetaDataFromFileContent(template); - fileMetadata = { ...templateMetadata, ...fileMetadata }; - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const attachFileContent = template.replace(regExp, ''); - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - getMetaDataFromFileContent(fileContent: string): Metadata { - let metadata: Metadata; - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const frontMatterRegExpResult = regExp.exec(fileContent); - if (!frontMatterRegExpResult) { - return {}; - } - let frontMatter = frontMatterRegExpResult[0]; - if (!frontMatter) { - return {}; - } - frontMatter = frontMatter.substring(4); - frontMatter = frontMatter.substring(0, frontMatter.length - 3); - - metadata = parseYaml(frontMatter) as Metadata; - - if (!metadata) { - metadata = {}; - } - - console.debug(`MDB | metadata read from file content`, metadata); - - return metadata; - } - - getMetadataFromFileCache(file: TFile): Metadata { - const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; - return structuredClone(metadata ?? {}); - } - - /** - * Creates a note in the vault. - * - * @param fileName - * @param fileContent - * @param options - */ - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { - // find and possibly create the folder set in settings or passed in folder - const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); - - if (!folder || !(folder instanceof TFolder)) { - throw new Error('MDB | invalid folder'); - } - - fileName = replaceIllegalFileNameCharactersInString(fileName); - const filePath = `${folder.path}/${fileName}.md`; - - // look if file already exists and ask if it should be overwritten - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file) { - const shouldOverwrite = await new Promise(resolve => { - new ConfirmOverwriteModal(this.app, fileName, resolve).open(); - }); - - if (!shouldOverwrite) { - throw new Error('MDB | file creation cancelled by user'); - } - - await this.app.vault.delete(file); - } - - // create the file - const targetFile = await this.app.vault.create(filePath, fileContent); - console.debug(`MDB | created new file at ${filePath}`); - - // open newly created file - if (options.openNote) { - const activeLeaf = this.app.workspace.getUnpinnedLeaf(); - if (!activeLeaf) { - console.warn('MDB | no active leaf, not opening newly created note'); - return targetFile; - } - await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); - } - - return targetFile; - } - - /** - * Update the active note by querying the API again. - * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. - */ - async updateActiveNote(onlyMetadata: boolean = false): Promise { - const activeFile = this.app.workspace.getActiveFile() ?? undefined; - if (!activeFile) { - throw new Error('MDB | there is no active note'); - } - - let metadata = this.getMetadataFromFileCache(activeFile); - metadata = this.modelPropertyMapper.convertObjectBack(metadata); - - console.debug(`MDB | read metadata`, metadata); - - if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { - throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); - } - - const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; - console.debug(`MDB | validOldMetadata`, validOldMetadata); - - const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); - console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); - - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); - if (!newMediaTypeModel) { - return; - } - - newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); - console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); - - if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); - } else { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); - } - } - - async loadSettings(): Promise { - const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; - 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 || [], - defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), - ); - - // Store as plain data for serialization - loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); - - this.settings = loadedSettings; - - await this.saveSettings(); - } - - async saveSettings(): Promise { - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - await this.saveData(this.settings); - } -} diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts deleted file mode 100644 index a8d15034..00000000 --- a/src/utils/ModalHelper.ts +++ /dev/null @@ -1,537 +0,0 @@ -import { Notice } from 'obsidian'; -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'; -import type { MediaType } from './MediaType'; - -export enum ModalResultCode { - SUCCESS = 'SUCCESS', - SKIP = 'SKIP', - CLOSE = 'CLOSE', - ERROR = 'ERROR', -} - -type ModalResult = - | { - code: ModalResultCode.CLOSE; - } - | { - code: ModalResultCode.ERROR; - error: Error; - } - | { - code: ModalResultCode.SUCCESS; - data: T; - }; - -type SkippableModalResult = - | ModalResult - | { - code: ModalResultCode.SKIP; - }; - -/** - * Object containing the data {@link ModalHelper.createSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type AdvancedSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createIdSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type IdSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createSelectModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SelectModalResult = SkippableModalResult; - -/** - * Object containing the data {@link ModalHelper.createPreviewModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type PreviewModalResult = ModalResult; - -/** - * The data the search modal returns. - * - query: the query string - * - types: the selected APIs - */ -export interface SearchModalData { - query: string; - types: MediaType[]; -} - -/** - * The data the advanced search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface AdvancedSearchModalData { - query: string; - apis: string[]; -} - -/** - * The data the id search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface IdSearchModalData { - query: string; - api: string; -} - -/** - * The data the select modal returns. - * - selected: the selected items - */ -export interface SelectModalData { - selected: MediaTypeModel[]; -} - -/** - * The data the preview modal returns. - * - confirmed: whether the selected element has been confirmed - */ -export interface PreviewModalData { - confirmed: boolean; -} - -/** - * Options for the search modal. - * - modalTitle: the title of the modal - * - preselectedTypes: a list of preselected Types - * - prefilledSearchString: prefilled query - */ -export interface SearchModalOptions { - modalTitle?: string; - preselectedTypes?: MediaType[]; - prefilledSearchString?: string; -} - -/** - * Options for the advanced search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface AdvancedSearchModalOptions { - modalTitle?: string; - preselectedAPIs?: string[]; - prefilledSearchString?: string; -} - -/** - * Options for the id search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface IdSearchModalOptions { - modalTitle?: string; - preselectedAPI?: string; - prefilledSearchString?: string; -} - -/** - * Options for the select modal. - * - modalTitle: the title of the modal - * - elements: the elements the user can select from - * - multiSelect: whether to allow multiselect - * - skipButton: whether to add a skip button to the modal - */ -export interface SelectModalOptions { - elements?: MediaTypeModel[]; - multiSelect?: boolean; - modalTitle?: string; - skipButton?: boolean; - description?: string; // Add this - submitButtonText?: string; // Add this too -} -/** - * Options for the preview modal. - * - modalTitle: the title of the modal - * - elements: the elements to preview - */ -export interface PreviewModalOptions { - modalTitle?: string; - elements?: MediaTypeModel[]; -} - -export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { - modalTitle: 'Media DB Search', - preselectedTypes: [], - prefilledSearchString: '', -}; - -export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { - modalTitle: 'Media DB Advanced Search', - preselectedAPIs: [], - prefilledSearchString: '', -}; - -export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { - modalTitle: 'Media DB Id Search', - preselectedAPI: undefined, - prefilledSearchString: '', -}; - -export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { - modalTitle: 'Media DB Search Results', - elements: [], - multiSelect: true, - skipButton: false, -}; - -export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { - modalTitle: 'Media DB Preview', - elements: [], -}; - -export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { - elements: [], - multiSelect: true, - modalTitle: '', - skipButton: false, - description: 'Select one or multiple search results.', - submitButtonText: 'Ok', -}; - -/** - * A class providing multiple usefull functions for dealing with the plugins modals. - */ -export class ModalHelper { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - /** - * Creates an {@link MediaDbSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult; searchModal: MediaDbSearchModal }> { - const modal = new MediaDbSearchModal(this.plugin, searchModalOptions); - const res: SearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { searchModalResult: res, searchModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSearchModal( - searchModalOptions: SearchModalOptions, - submitCallback: (searchModalData: SearchModalData) => Promise, - ): Promise { - const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions); - console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`); - - if (searchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(searchModalResult.error); - new Notice(searchModalResult.error.toString()); - searchModal.close(); - return undefined; - } - - if (searchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(searchModalResult.data); - searchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - searchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - ): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult; advancedSearchModal: MediaDbAdvancedSearchModal }> { - const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions); - const res: AdvancedSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { advancedSearchModalResult: res, advancedSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbAdvancedSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise, - ): Promise { - const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions); - console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`); - - if (advancedSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(advancedSearchModalResult.error); - new Notice(advancedSearchModalResult.error.toString()); - advancedSearchModal.close(); - return undefined; - } - - if (advancedSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(advancedSearchModalResult.data); - advancedSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - advancedSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbIdSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult; idSearchModal: MediaDbIdSearchModal }> { - const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions); - const res: IdSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { idSearchModalResult: res, idSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbIdSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openIdSearchModal( - idSearchModalOptions: IdSearchModalOptions, - submitCallback: (idSearchModalData: IdSearchModalData) => Promise, - ): Promise { - const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions); - console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`); - - if (idSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(idSearchModalResult.error); - new Notice(idSearchModalResult.error.toString()); - idSearchModal.close(); - return undefined; - } - - if (idSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes = await submitCallback(idSearchModalResult.data); - idSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - idSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbSearchResultModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @returns the user input or nothing and a reference to the modal. - */ - async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult; selectModal: MediaDbSearchResultModal }> { - const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions); - const res: SelectModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setSkipCallback(() => resolve({ code: ModalResultCode.SKIP })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { selectModalResult: res, selectModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchResultModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @param submitCallback the callback that gets executed after the modal has been submitted, but before it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSelectModal( - selectModalOptions: SelectModalOptions, - submitCallback: (selectModalData: SelectModalData) => Promise, - ): Promise { - const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions); - console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`); - - if (selectModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(selectModalResult.error); - new Notice(selectModalResult.error.toString()); - selectModal.close(); - return undefined; - } - - if (selectModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - if (selectModalResult.code === ModalResultCode.SKIP) { - // selection was skipped - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(selectModalResult.data); - selectModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - selectModal.close(); - return; - } - } - - async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult; previewModal: MediaDbPreviewModal }> { - //todo: handle attachFile for existing files - const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions); - const res: PreviewModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { previewModalResult: res, previewModal: modal }; - } - - async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise): Promise { - const { previewModalResult, previewModal } = await this.createPreviewModal(previewModalOptions); - console.debug(`MDB | previewModal closed with code ${previewModalResult.code}`); - - if (previewModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(previewModalResult.error); - new Notice(previewModalResult.error.toString()); - previewModal.close(); - return false; - } - - if (previewModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return false; - } - - try { - const callbackRes: boolean = await submitCallback(previewModalResult.data); - previewModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - previewModal.close(); - return true; - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 49b6fcbb..7ce937bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,12 @@ { "compilerOptions": { + "paths": { + "packages/*": ["./packages/*"], + "tests/*": ["./tests/*"] + }, "module": "ESNext", - "target": "ESNext", + "target": "ES2022", + "noEmit": true, "allowJs": true, "checkJs": true, "noImplicitAny": true, @@ -22,5 +27,5 @@ "jsxImportSource": "solid-js", "types": ["vite/client"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"] + "include": ["packages/obsidian/**/*.ts", "packages/obsidian/**/*.tsx"] } diff --git a/vite.config.ts b/vite.config.mts similarity index 70% rename from vite.config.ts rename to vite.config.mts index 3a8f24b6..e42ec6d9 100644 --- a/vite.config.ts +++ b/vite.config.mts @@ -1,20 +1,20 @@ -import { defineConfig } from 'vite'; -import solidPlugin from 'vite-plugin-solid'; -import { builtinModules } from 'node:module'; -import { getBuildBanner } from './automation/build/buildBanner'; +import { UserConfig, defineConfig } from 'vite'; +import solid from 'vite-plugin-solid'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; -import manifest from './manifest.json' with { type: 'json' }; import path from 'path'; +import { builtinModules } from 'node:module'; +import { dtsBundlePlugin, getBuildBanner } from '@lemons_dev/lemons-obsidian-plugin-automation'; +import manifest from './manifest.json' with { type: 'json' }; -const entryFile = 'src/main.ts'; +const entryFile = 'packages/obsidian/src/main.ts'; -export default defineConfig(({ mode }) => { +export default defineConfig(async ({ mode }) => { const prod = mode === 'production'; - const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}`; + const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}/`; - const plugins = [ - solidPlugin(), + let plugins = [ + solid(), banner({ outDir: outDir, content: getBuildBanner(prod ? 'Release Build' : 'Dev Build', version => version), @@ -30,13 +30,16 @@ export default defineConfig(({ mode }) => { ]; return { - plugins, - + plugins: plugins, resolve: { alias: { - src: path.resolve(__dirname, './src'), + packages: path.resolve(__dirname, './packages'), }, }, + define: { + // Disable all logs and debug statements in production, but keep warnings and errors + __LOG_LEVEL__: prod ? 2 : 4, + }, build: { lib: { entry: path.resolve(__dirname, entryFile), @@ -45,11 +48,12 @@ export default defineConfig(({ mode }) => { formats: ['cjs'], }, minify: prod, + target: 'es2022', sourcemap: prod ? false : 'inline', cssCodeSplit: false, emptyOutDir: false, outDir: '', - rollupOptions: { + rolldownOptions: { input: { main: path.resolve(__dirname, entryFile), }, @@ -76,5 +80,5 @@ export default defineConfig(({ mode }) => { ], }, }, - }; + } as UserConfig; }); From e058bcb43583f8c91938617fd85c19a0be9faf4d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 6 May 2026 17:03:31 +0200 Subject: [PATCH 19/35] cleanup of new result code --- packages/obsidian/src/api/APIManager.ts | 22 ++++---- packages/obsidian/src/api/APIModel.ts | 6 +-- .../obsidian/src/api/apis/BoardGameGeekAPI.ts | 30 +++++------ .../obsidian/src/api/apis/ComicVineAPI.ts | 24 ++++----- packages/obsidian/src/api/apis/IGDBAPI.ts | 36 ++++++------- packages/obsidian/src/api/apis/MALAPI.ts | 16 +++--- packages/obsidian/src/api/apis/MALAPIManga.ts | 14 ++--- .../obsidian/src/api/apis/MusicBrainzAPI.ts | 28 +++++----- packages/obsidian/src/api/apis/OMDbAPI.ts | 40 +++++++------- .../obsidian/src/api/apis/OpenLibraryAPI.ts | 22 ++++---- packages/obsidian/src/api/apis/RAWGAPI.ts | 24 ++++----- packages/obsidian/src/api/apis/SteamAPI.ts | 26 ++++----- .../obsidian/src/api/apis/TMDBMovieAPI.ts | 32 +++++------ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 54 +++++++++---------- .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 32 +++++------ packages/obsidian/src/api/apis/VNDBAPI.ts | 32 +++++------ .../obsidian/src/api/apis/WikipediaAPI.ts | 22 ++++---- packages/obsidian/src/utils/ErrorReporter.ts | 12 ++--- .../src/utils/{AppError.ts => MDBError.ts} | 12 ++--- .../obsidian/src/utils/MediaDbEntryHelper.ts | 30 +++++------ .../obsidian/src/utils/MediaDbFileHelper.ts | 28 +++++----- packages/obsidian/src/utils/ModalHelper.ts | 32 +++++------ packages/obsidian/src/utils/result.ts | 52 ++++++++++++------ 23 files changed, 323 insertions(+), 303 deletions(-) rename packages/obsidian/src/utils/{AppError.ts => MDBError.ts} (62%) diff --git a/packages/obsidian/src/api/APIManager.ts b/packages/obsidian/src/api/APIManager.ts index de9ba412..65309fd3 100644 --- a/packages/obsidian/src/api/APIManager.ts +++ b/packages/obsidian/src/api/APIManager.ts @@ -1,14 +1,14 @@ import type { APIModel } from 'packages/obsidian/src/api/APIModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; export interface ApiQueryOk { items: MediaTypeModel[]; - warnings: AppError[]; + warnings: MDBError[]; } export class APIManager { @@ -24,14 +24,14 @@ export class APIManager { * @param query * @param apisToQuery */ - async query(query: string, apisToQuery: string[]): Promise> { + async query(query: string, apisToQuery: string[]): Promise> { Logger.debug(`MDB | api manager queried with "${query}"`); const apis = this.apis.filter(api => apisToQuery.includes(api.apiName)); const results = await Promise.all(apis.map(api => api.searchByTitle(query))); const items: MediaTypeModel[] = []; - const warnings: AppError[] = []; + const warnings: MDBError[] = []; for (const result of results) { if (result.ok) { items.push(...result.value); @@ -43,8 +43,8 @@ export class APIManager { if (items.length === 0 && warnings.length > 0) { // If all APIs failed, surface an error (using the first as representative) return err( - toAppError(warnings[0], { - kind: AppErrorKind.Api, + toMdbError(warnings[0], { + kind: MDBErrorKind.Api, message: 'Failed to query APIs', userMessage: 'Failed to query APIs', context: { query, apisToQuery }, @@ -64,7 +64,7 @@ export class APIManager { * * @param item */ - async queryDetailedInfo(item: MediaTypeModel): Promise> { + async queryDetailedInfo(item: MediaTypeModel): Promise> { return await this.queryDetailedInfoById(item.id, item.dataSource); } @@ -74,7 +74,7 @@ export class APIManager { * @param id * @param apiName */ - async queryDetailedInfoById(id: string, apiName: string): Promise> { + async queryDetailedInfoById(id: string, apiName: string): Promise> { for (const api of this.apis) { if (api.apiName === apiName) { const result = await api.getById(id); @@ -88,8 +88,8 @@ export class APIManager { } return err( - toAppError(new Error(`API not found: ${apiName}`), { - kind: AppErrorKind.Validation, + toMdbError(new Error(`API not found: ${apiName}`), { + kind: MDBErrorKind.Validation, message: `API not found: ${apiName}`, userMessage: `API not found: ${apiName}`, context: { apiName, id }, diff --git a/packages/obsidian/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts index 7527e99e..c5bfe5d9 100644 --- a/packages/obsidian/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,6 +1,6 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; @@ -16,9 +16,9 @@ export abstract class APIModel { * * @param title the title to query for */ - abstract searchByTitle(title: string): Promise>; + abstract searchByTitle(title: string): Promise>; - abstract getById(id: string): Promise>; + abstract getById(id: string): Promise>; abstract getDisabledMediaTypes(): MediaType[]; diff --git a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts index 889b779f..331ed409 100644 --- a/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts +++ b/packages/obsidian/src/api/apis/BoardGameGeekAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { BoardGameModel } from 'packages/obsidian/src/models/BoardGameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -25,12 +25,12 @@ export class BoardGameGeekAPI extends APIModel { this.types = [MediaType.BoardGame]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -46,8 +46,8 @@ export class BoardGameGeekAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -61,7 +61,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -70,7 +70,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -103,12 +103,12 @@ export class BoardGameGeekAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.BoardgameGeekKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -124,8 +124,8 @@ export class BoardGameGeekAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -139,7 +139,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -148,7 +148,7 @@ export class BoardGameGeekAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -162,7 +162,7 @@ export class BoardGameGeekAPI extends APIModel { const boardgame = response.querySelector('boardgame'); if (!boardgame) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received invalid data from ${this.apiName}.`, userMessage: `Received invalid data from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/ComicVineAPI.ts b/packages/obsidian/src/api/apis/ComicVineAPI.ts index 0babc397..3a8a0b18 100644 --- a/packages/obsidian/src/api/apis/ComicVineAPI.ts +++ b/packages/obsidian/src/api/apis/ComicVineAPI.ts @@ -5,9 +5,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -27,12 +27,12 @@ export class ComicVineAPI extends APIModel { this.types = [MediaType.ComicManga]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -45,8 +45,8 @@ export class ComicVineAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -59,7 +59,7 @@ export class ComicVineAPI extends APIModel { // console.debug(fetchData); if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -85,12 +85,12 @@ export class ComicVineAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.ComicVineKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -103,8 +103,8 @@ export class ComicVineAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -119,7 +119,7 @@ export class ComicVineAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, diff --git a/packages/obsidian/src/api/apis/IGDBAPI.ts b/packages/obsidian/src/api/apis/IGDBAPI.ts index acfbf61d..a63958c8 100644 --- a/packages/obsidian/src/api/apis/IGDBAPI.ts +++ b/packages/obsidian/src/api/apis/IGDBAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -60,7 +60,7 @@ export class IGDBAPI extends APIModel { this.types = [MediaType.Game]; } - private async getAuthToken(): Promise> { + private async getAuthToken(): Promise> { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return ok(this.accessToken); @@ -69,7 +69,7 @@ export class IGDBAPI extends APIModel { if (!clientId || !clientSecret) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID or Client Secret for ${this.apiName} missing.`, userMessage: `Client ID or Client Secret for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -82,8 +82,8 @@ export class IGDBAPI extends APIModel { method: 'POST', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying Twitch auth for ${this.apiName}`, userMessage: `Network error querying Twitch auth for ${this.apiName}`, context: { apiName: this.apiName }, @@ -96,7 +96,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Auth failed for ${this.apiName}. Check Credentials.`, userMessage: `Auth failed for ${this.apiName}. Check Credentials.`, context: { apiName: this.apiName, status: response.status }, @@ -109,12 +109,12 @@ export class IGDBAPI extends APIModel { return ok(this.accessToken); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -134,8 +134,8 @@ export class IGDBAPI extends APIModel { body: queryBody, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -147,7 +147,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -172,12 +172,12 @@ export class IGDBAPI extends APIModel { ); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const clientId = this.plugin.app.secretStorage.getSecret(this.plugin.settings.IGDBClientId); if (!clientId) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Client ID for ${this.apiName} missing.`, userMessage: `Client ID for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -197,8 +197,8 @@ export class IGDBAPI extends APIModel { body: queryBody, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -210,7 +210,7 @@ export class IGDBAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, @@ -220,7 +220,7 @@ export class IGDBAPI extends APIModel { const data = response.json as IGDBGame[]; if (!data || data.length === 0) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No result found for ID ${id}`, userMessage: `No result found for ID ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MALAPI.ts b/packages/obsidian/src/api/apis/MALAPI.ts index 11d1c02f..1461b970 100644 --- a/packages/obsidian/src/api/apis/MALAPI.ts +++ b/packages/obsidian/src/api/apis/MALAPI.ts @@ -4,9 +4,9 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -33,7 +33,7 @@ export class MALAPI extends APIModel { this.typeMappings.set('ova', 'ova'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -51,7 +51,7 @@ export class MALAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -108,7 +108,7 @@ export class MALAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -124,7 +124,7 @@ export class MALAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -135,7 +135,7 @@ export class MALAPI extends APIModel { if (result === undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id} in ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -248,7 +248,7 @@ export class MALAPI extends APIModel { } return err({ - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: `MDB | Unknown media type for id ${id}`, userMessage: `Unknown media type for id ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MALAPIManga.ts b/packages/obsidian/src/api/apis/MALAPIManga.ts index 424449b3..f864fa55 100644 --- a/packages/obsidian/src/api/apis/MALAPIManga.ts +++ b/packages/obsidian/src/api/apis/MALAPIManga.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ComicMangaModel } from 'packages/obsidian/src/models/ComicMangaModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -35,7 +35,7 @@ export class MALAPIManga extends APIModel { this.typeMappings.set('novel', 'novel'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -53,7 +53,7 @@ export class MALAPIManga extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -106,7 +106,7 @@ export class MALAPIManga extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); @@ -122,7 +122,7 @@ export class MALAPIManga extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -133,7 +133,7 @@ export class MALAPIManga extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id} in ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts index a61d7d0b..2c4a534b 100644 --- a/packages/obsidian/src/api/apis/MusicBrainzAPI.ts +++ b/packages/obsidian/src/api/apis/MusicBrainzAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MusicReleaseModel } from 'packages/obsidian/src/models/MusicReleaseModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -108,7 +108,7 @@ export class MusicBrainzAPI extends APIModel { this.types = [MediaType.MusicRelease]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; @@ -121,8 +121,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -137,7 +137,7 @@ export class MusicBrainzAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -172,7 +172,7 @@ export class MusicBrainzAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); // Fetch release group @@ -185,8 +185,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -199,7 +199,7 @@ export class MusicBrainzAPI extends APIModel { if (groupResponse.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${groupResponse.status} from ${this.apiName}.`, userMessage: `Received status code ${groupResponse.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: groupResponse.status, id }, @@ -212,7 +212,7 @@ export class MusicBrainzAPI extends APIModel { const firstRelease = result.releases?.[0]; if (!firstRelease) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | No releases found in release group.', userMessage: 'No releases found in release group.', context: { apiName: this.apiName, id }, @@ -231,8 +231,8 @@ export class MusicBrainzAPI extends APIModel { }, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id, releaseId: firstRelease.id }, @@ -245,7 +245,7 @@ export class MusicBrainzAPI extends APIModel { if (releaseResponse.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`, userMessage: `Received status code ${releaseResponse.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: releaseResponse.status, id, releaseId: firstRelease.id }, diff --git a/packages/obsidian/src/api/apis/OMDbAPI.ts b/packages/obsidian/src/api/apis/OMDbAPI.ts index 24393785..ab57f8c8 100644 --- a/packages/obsidian/src/api/apis/OMDbAPI.ts +++ b/packages/obsidian/src/api/apis/OMDbAPI.ts @@ -5,9 +5,9 @@ import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -79,13 +79,13 @@ export class OMDbAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -98,8 +98,8 @@ export class OMDbAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -113,7 +113,7 @@ export class OMDbAPI extends APIModel { if (response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -121,7 +121,7 @@ export class OMDbAPI extends APIModel { } if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -132,7 +132,7 @@ export class OMDbAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -145,7 +145,7 @@ export class OMDbAPI extends APIModel { } return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received error from ${this.apiName}: ${data.Error}`, userMessage: `${data.Error}`, context: { apiName: this.apiName }, @@ -203,13 +203,13 @@ export class OMDbAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.OMDbKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -222,8 +222,8 @@ export class OMDbAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -237,7 +237,7 @@ export class OMDbAPI extends APIModel { if (response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -245,7 +245,7 @@ export class OMDbAPI extends APIModel { } if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, @@ -256,7 +256,7 @@ export class OMDbAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -265,7 +265,7 @@ export class OMDbAPI extends APIModel { if (result.Response === 'False') { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received error from ${this.apiName}: ${result.Error}`, userMessage: `${result.Error}`, context: { apiName: this.apiName, id }, @@ -275,7 +275,7 @@ export class OMDbAPI extends APIModel { const type = this.typeMappings.get(result.Type.toLowerCase()); if (type === undefined) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `${result.Type.toLowerCase()} is an unsupported type.`, userMessage: `${result.Type.toLowerCase()} is an unsupported type.`, context: { apiName: this.apiName, id, type: result.Type }, @@ -375,7 +375,7 @@ export class OMDbAPI extends APIModel { } return err({ - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: `MDB | Unknown media type for id ${id}`, userMessage: `Unknown media type for id ${id}`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index 6a52bdba..08df3be2 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { BookModel } from 'packages/obsidian/src/models/BookModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -48,7 +48,7 @@ export class OpenLibraryAPI extends APIModel { this.types = [MediaType.Book]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const client = createClient({ baseUrl: 'https://openlibrary.org/' }); @@ -63,8 +63,8 @@ export class OpenLibraryAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -78,7 +78,7 @@ export class OpenLibraryAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -109,7 +109,7 @@ export class OpenLibraryAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const client = createClient({ baseUrl: 'https://openlibrary.org/' }); @@ -125,8 +125,8 @@ export class OpenLibraryAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -140,7 +140,7 @@ export class OpenLibraryAPI extends APIModel { if (response.error !== undefined) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -155,7 +155,7 @@ export class OpenLibraryAPI extends APIModel { const result = data.docs?.[0]; if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data found for ID ${id} in ${this.apiName}.`, userMessage: `No data found for ID ${id}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/RAWGAPI.ts b/packages/obsidian/src/api/apis/RAWGAPI.ts index 21d603ba..a3f13be4 100644 --- a/packages/obsidian/src/api/apis/RAWGAPI.ts +++ b/packages/obsidian/src/api/apis/RAWGAPI.ts @@ -3,8 +3,8 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -40,11 +40,11 @@ export class RAWGAPI extends APIModel { this.types = [MediaType.Game]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -57,8 +57,8 @@ export class RAWGAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -71,7 +71,7 @@ export class RAWGAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Error ${response.status} from ${this.apiName}.`, userMessage: `Error ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status }, @@ -95,11 +95,11 @@ export class RAWGAPI extends APIModel { ); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.RAWGAPIKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -112,8 +112,8 @@ export class RAWGAPI extends APIModel { method: 'GET', }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -126,7 +126,7 @@ export class RAWGAPI extends APIModel { const response = responseResult.value; if (response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Error ${response.status} from ${this.apiName}.`, userMessage: `Error ${response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.status, id }, diff --git a/packages/obsidian/src/api/apis/SteamAPI.ts b/packages/obsidian/src/api/apis/SteamAPI.ts index 8a47efa3..e3317096 100644 --- a/packages/obsidian/src/api/apis/SteamAPI.ts +++ b/packages/obsidian/src/api/apis/SteamAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -148,7 +148,7 @@ export class SteamAPI extends APIModel { this.typeMappings.set('game', 'game'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; @@ -157,8 +157,8 @@ export class SteamAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -171,7 +171,7 @@ export class SteamAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -200,7 +200,7 @@ export class SteamAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; @@ -209,8 +209,8 @@ export class SteamAPI extends APIModel { url: searchUrl, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -223,7 +223,7 @@ export class SteamAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -242,7 +242,7 @@ export class SteamAPI extends APIModel { } if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | API returned invalid data.', userMessage: 'Steam returned invalid data.', context: { apiName: this.apiName, id }, @@ -254,8 +254,8 @@ export class SteamAPI extends APIModel { // Check if a poster version of the image exists, else use the header image const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; const existsResult = await fromPromise(imageUrlExists(imageUrl), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to validate image URL for ${this.apiName}`, userMessage: `Failed to validate image URL for ${this.apiName}`, context: { apiName: this.apiName, id, imageUrl }, diff --git a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts index 9f2dbf15..ca3e89e9 100644 --- a/packages/obsidian/src/api/apis/TMDBMovieAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBMovieAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { MovieModel } from 'packages/obsidian/src/models/MovieModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -59,13 +59,13 @@ export class TMDBMovieAPI extends APIModel { this.typeMappings.set('movie', 'movie'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -87,8 +87,8 @@ export class TMDBMovieAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -103,7 +103,7 @@ export class TMDBMovieAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -111,7 +111,7 @@ export class TMDBMovieAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -122,7 +122,7 @@ export class TMDBMovieAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -153,13 +153,13 @@ export class TMDBMovieAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -181,8 +181,8 @@ export class TMDBMovieAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -197,7 +197,7 @@ export class TMDBMovieAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -205,7 +205,7 @@ export class TMDBMovieAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -216,7 +216,7 @@ export class TMDBMovieAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts index f2710e3e..fd6a37a7 100644 --- a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -63,13 +63,13 @@ export class TMDBSeasonAPI extends APIModel { this.typeMappings.set('tv', 'season'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -91,8 +91,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -106,7 +106,7 @@ export class TMDBSeasonAPI extends APIModel { if (searchResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -115,7 +115,7 @@ export class TMDBSeasonAPI extends APIModel { if (searchResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${searchResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: searchResponse.response.status }, @@ -170,11 +170,11 @@ export class TMDBSeasonAPI extends APIModel { } // Fetch all seasons for a given series - async getSeasonsForSeries(tvId: string): Promise> { + async getSeasonsForSeries(tvId: string): Promise> { const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, tvId }, @@ -193,8 +193,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, tvId }, @@ -208,7 +208,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, tvId }, @@ -217,7 +217,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seriesResponse.response.status, tvId }, @@ -252,13 +252,13 @@ export class TMDBSeasonAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -269,7 +269,7 @@ export class TMDBSeasonAPI extends APIModel { const m = /^(\d+)\/season\/(\d+)$/.exec(id); if (!m) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Invalid season id "${id}". Expected format "/season/".`, userMessage: `Invalid season id "${id}".`, context: { apiName: this.apiName, id }, @@ -296,8 +296,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -311,7 +311,7 @@ export class TMDBSeasonAPI extends APIModel { if (seasonResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -320,7 +320,7 @@ export class TMDBSeasonAPI extends APIModel { if (seasonResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seasonResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seasonResponse.response.status, id }, @@ -330,7 +330,7 @@ export class TMDBSeasonAPI extends APIModel { const seasonData = seasonResponse.data; if (!seasonData) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, @@ -351,8 +351,8 @@ export class TMDBSeasonAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -366,7 +366,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -375,7 +375,7 @@ export class TMDBSeasonAPI extends APIModel { if (seriesResponse.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`, userMessage: `Received status code ${seriesResponse.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: seriesResponse.response.status, id }, @@ -386,7 +386,7 @@ export class TMDBSeasonAPI extends APIModel { if (!seriesData) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts index 40f68d7c..61f727fd 100644 --- a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeriesModel } from 'packages/obsidian/src/models/SeriesModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -50,13 +50,13 @@ export class TMDBSeriesAPI extends APIModel { this.typeMappings.set('tv', 'series'); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName }, @@ -78,8 +78,8 @@ export class TMDBSeriesAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, title }, @@ -93,7 +93,7 @@ export class TMDBSeriesAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName }, @@ -101,7 +101,7 @@ export class TMDBSeriesAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status }, @@ -112,7 +112,7 @@ export class TMDBSeriesAPI extends APIModel { if (!data) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName }, @@ -143,13 +143,13 @@ export class TMDBSeriesAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const key = this.plugin.app.secretStorage.getSecret(this.plugin.settings.TMDBKeyId); if (!key) { return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | API key for ${this.apiName} missing.`, userMessage: `API key for ${this.apiName} missing.`, context: { apiName: this.apiName, id }, @@ -171,8 +171,8 @@ export class TMDBSeriesAPI extends APIModel { fetch: obsidianFetch, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -186,7 +186,7 @@ export class TMDBSeriesAPI extends APIModel { if (response.response.status === 401) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Authentication for ${this.apiName} failed. Check the API key.`, userMessage: `Authentication for ${this.apiName} failed. Check the API key.`, context: { apiName: this.apiName, id }, @@ -194,7 +194,7 @@ export class TMDBSeriesAPI extends APIModel { } if (response.response.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: response.response.status, id }, @@ -205,7 +205,7 @@ export class TMDBSeriesAPI extends APIModel { if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/VNDBAPI.ts b/packages/obsidian/src/api/apis/VNDBAPI.ts index eae27de9..0506f4b7 100644 --- a/packages/obsidian/src/api/apis/VNDBAPI.ts +++ b/packages/obsidian/src/api/apis/VNDBAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import { GameModel } from 'packages/obsidian/src/models/GameModel'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -110,7 +110,7 @@ export class VNDBAPI extends APIModel { * @returns A JSON object representing the query response. * @see {@link https://api.vndb.org/kana#api-structure} */ - private async postQuery(endpoint: string, body: string): Promise> { + private async postQuery(endpoint: string, body: string): Promise> { const fetchDataResult = await fromPromise( requestUrl({ url: `${this.apiUrl}${endpoint}`, @@ -120,8 +120,8 @@ export class VNDBAPI extends APIModel { throw: false, }), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, context: { apiName: this.apiName, endpoint }, @@ -136,42 +136,42 @@ export class VNDBAPI extends APIModel { switch (fetchData.status) { case 400: return err({ - kind: AppErrorKind.Validation, + kind: MDBErrorKind.Validation, message: `MDB | Invalid request body or query [${fetchData.text}].`, userMessage: 'Invalid VNDB request.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 404: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | Invalid API path or HTTP method.', userMessage: 'VNDB endpoint not found.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 429: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB throttled the request.', userMessage: 'VNDB throttled the request. Please try again later.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 500: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB server error.', userMessage: 'VNDB server error.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); case 502: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: 'MDB | VNDB server is down.', userMessage: 'VNDB server is down.', context: { apiName: this.apiName, endpoint, status: fetchData.status }, }); default: return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, endpoint, status: fetchData.status }, @@ -187,7 +187,7 @@ export class VNDBAPI extends APIModel { * Queries visual novel entries. * @see {@link https://api.vndb.org/kana#post-vn} */ - private async postVNQuery(body: string): Promise> { + private async postVNQuery(body: string): Promise> { const result = await this.postQuery('/vn', body); return result.ok ? ok(result.value as VNJSONResponse) : err(result.error); } @@ -197,12 +197,12 @@ export class VNDBAPI extends APIModel { * Queries release entries. * @see {@link https://api.vndb.org/kana#post-release} */ - private async postReleaseQuery(body: string): Promise> { + private async postReleaseQuery(body: string): Promise> { const result = await this.postQuery('/release', body); return result.ok ? ok(result.value as ReleaseJSONResponse) : err(result.error); } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); /* SFW Filter: has ANY official&&complete&&standalone&&SFW release @@ -253,7 +253,7 @@ export class VNDBAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const vnDataResult = await this.postVNQuery(`{ @@ -267,7 +267,7 @@ export class VNDBAPI extends APIModel { if (vnData.results.length !== 1) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Expected 1 result from query, got ${vnData.results.length}.`, userMessage: 'Unexpected VNDB response.', context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/api/apis/WikipediaAPI.ts b/packages/obsidian/src/api/apis/WikipediaAPI.ts index 27657860..392c53e3 100644 --- a/packages/obsidian/src/api/apis/WikipediaAPI.ts +++ b/packages/obsidian/src/api/apis/WikipediaAPI.ts @@ -3,9 +3,9 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { WikiModel } from 'packages/obsidian/src/models/WikiModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; @@ -53,7 +53,7 @@ export class WikipediaAPI extends APIModel { this.types = [MediaType.Wiki]; } - async searchByTitle(title: string): Promise> { + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; @@ -66,7 +66,7 @@ export class WikipediaAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status }, @@ -75,8 +75,8 @@ export class WikipediaAPI extends APIModel { const response = fetchData as { status: number; json(): Promise }; const dataResult = await fromPromise(response.json(), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to parse response from ${this.apiName}`, userMessage: `Failed to parse response from ${this.apiName}`, context: { apiName: this.apiName }, @@ -105,7 +105,7 @@ export class WikipediaAPI extends APIModel { return ok(ret); } - async getById(id: string): Promise> { + async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; @@ -117,7 +117,7 @@ export class WikipediaAPI extends APIModel { if (fetchData.status !== 200) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | Received status code ${fetchData.status} from ${this.apiName}.`, userMessage: `Received status code ${fetchData.status} from ${this.apiName}.`, context: { apiName: this.apiName, status: fetchData.status, id }, @@ -126,8 +126,8 @@ export class WikipediaAPI extends APIModel { const response = fetchData as { status: number; json(): Promise }; const dataResult = await fromPromise(response.json(), cause => - toAppError(cause, { - kind: AppErrorKind.Network, + toMdbError(cause, { + kind: MDBErrorKind.Network, message: `MDB | Failed to parse response from ${this.apiName}`, userMessage: `Failed to parse response from ${this.apiName}`, context: { apiName: this.apiName, id }, @@ -141,7 +141,7 @@ export class WikipediaAPI extends APIModel { const result = Object.values(data?.query?.pages)[0]; if (!result) { return err({ - kind: AppErrorKind.Api, + kind: MDBErrorKind.Api, message: `MDB | No data received from ${this.apiName}.`, userMessage: `No data received from ${this.apiName}.`, context: { apiName: this.apiName, id }, diff --git a/packages/obsidian/src/utils/ErrorReporter.ts b/packages/obsidian/src/utils/ErrorReporter.ts index 4155ae1a..dc8b6934 100644 --- a/packages/obsidian/src/utils/ErrorReporter.ts +++ b/packages/obsidian/src/utils/ErrorReporter.ts @@ -1,22 +1,22 @@ import { Notice } from 'obsidian'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; export class ErrorReporter { - notice(error: AppError): void { + notice(error: MDBError): void { const message = error.userMessage ?? error.message; new Notice(message); } - log(error: AppError): void { + log(error: MDBError): void { Logger.warn('MDB | error', error); } - report(error: AppError): void { + report(error: MDBError): void { this.log(error); - if (error.kind !== AppErrorKind.Cancelled) { + if (error.kind !== MDBErrorKind.Cancelled) { this.notice(error); } } diff --git a/packages/obsidian/src/utils/AppError.ts b/packages/obsidian/src/utils/MDBError.ts similarity index 62% rename from packages/obsidian/src/utils/AppError.ts rename to packages/obsidian/src/utils/MDBError.ts index 50db6841..84f08a20 100644 --- a/packages/obsidian/src/utils/AppError.ts +++ b/packages/obsidian/src/utils/MDBError.ts @@ -1,4 +1,4 @@ -export enum AppErrorKind { +export enum MDBErrorKind { Validation = 'Validation', Api = 'Api', Network = 'Network', @@ -8,17 +8,15 @@ export enum AppErrorKind { Unexpected = 'Unexpected', } -export interface AppError { - kind: AppErrorKind; +export interface MDBError { + kind: MDBErrorKind; message: string; userMessage?: string; cause?: unknown; context?: Record; } -export const appError = (error: AppError): AppError => error; - -export const toAppError = (cause: unknown, fallback: Omit): AppError => { +export function toMdbError(cause: unknown, fallback: Omit): MDBError { const message = cause instanceof Error ? cause.message : String(cause); return { ...fallback, message: fallback.message || message, cause }; -}; +} diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 222a11c0..2f558d3a 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -5,8 +5,8 @@ import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/Medi import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; @@ -17,7 +17,7 @@ export class MediaDbEntryHelper { this.plugin = plugin; } - private reportAppError(error: AppError): void { + private reportMdbError(error: MDBError): void { this.plugin.errorReporter.report(error); } @@ -29,12 +29,12 @@ export class MediaDbEntryHelper { const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } @@ -66,18 +66,18 @@ export class MediaDbEntryHelper { const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } const filteredSearchResults = apiSearchResults.value.items.filter(result => types.includes(result.getMediaType())); if (filteredSearchResults.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' }); return; } @@ -121,12 +121,12 @@ export class MediaDbEntryHelper { const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); if (!apiSearchResults.ok) { - this.reportAppError(apiSearchResults.error); + this.reportMdbError(apiSearchResults.error); return; } if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) { - this.reportAppError({ kind: AppErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); + this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' }); return; } @@ -160,7 +160,7 @@ export class MediaDbEntryHelper { const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); if (!queriedIdResult.ok) { - this.reportAppError(queriedIdResult.error); + this.reportMdbError(queriedIdResult.error); return; } @@ -183,7 +183,7 @@ export class MediaDbEntryHelper { const createNoteResult = await this.plugin.fileHelper.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); if (!createNoteResult.ok) { - this.reportAppError(createNoteResult.error); + this.reportMdbError(createNoteResult.error); } } @@ -195,7 +195,7 @@ export class MediaDbEntryHelper { if (result.ok && result.value) { detailModels.push(result.value); } else if (!result.ok) { - this.reportAppError(result.error); + this.reportMdbError(result.error); } } @@ -228,7 +228,7 @@ export class MediaDbEntryHelper { const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); if (!allSeasonsResult.ok) { - this.reportAppError(allSeasonsResult.error); + this.reportMdbError(allSeasonsResult.error); new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); return false; } @@ -276,7 +276,7 @@ export class MediaDbEntryHelper { if (seasonModel) { const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); if (!fullMetadataResult.ok) { - this.reportAppError(fullMetadataResult.error); + this.reportMdbError(fullMetadataResult.error); new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); return; } diff --git a/packages/obsidian/src/utils/MediaDbFileHelper.ts b/packages/obsidian/src/utils/MediaDbFileHelper.ts index 42f1bdd4..4c7db6f2 100644 --- a/packages/obsidian/src/utils/MediaDbFileHelper.ts +++ b/packages/obsidian/src/utils/MediaDbFileHelper.ts @@ -4,9 +4,9 @@ import { Notice, normalizePath, parseYaml, requestUrl, stringifyYaml } from 'obs import type MediaDbPlugin from 'packages/obsidian/src/main'; import { ConfirmOverwriteModal } from 'packages/obsidian/src/modals/ConfirmOverwriteModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; import { Logger } from 'packages/obsidian/src/utils/Logger'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; import { err, ok } from 'packages/obsidian/src/utils/result'; @@ -29,7 +29,7 @@ export class MediaDbFileHelper { this.plugin = plugin; } - async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise> { const results = await Promise.all(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile }))); const failures = results.filter(result => !result.ok); @@ -45,7 +45,7 @@ export class MediaDbFileHelper { return ok(undefined); } - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise> { Logger.debug('MDB | creating new note'); options.openNote = this.plugin.settings.openNoteInNewTab; @@ -58,7 +58,7 @@ export class MediaDbFileHelper { } const fileContentResult = await this.attempt(() => this.generateMediaDbNoteContents(mediaTypeModel, options), { - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: 'Failed to generate note contents', userMessage: 'Failed to generate note contents', }); @@ -67,7 +67,7 @@ export class MediaDbFileHelper { } const folderResult = await this.attempt(() => this.plugin.mediaTypeManager.getFolder(mediaTypeModel, this.plugin.app), { - kind: AppErrorKind.Vault, + kind: MDBErrorKind.Vault, message: 'Failed to determine note folder', userMessage: 'Failed to determine note folder', }); @@ -84,7 +84,7 @@ export class MediaDbFileHelper { if (this.plugin.settings.enableTemplaterIntegration) { const templaterResult = await this.attempt(() => useTemplaterPluginInFile(this.plugin.app, targetFileResult.value), { - kind: AppErrorKind.Unexpected, + kind: MDBErrorKind.Unexpected, message: 'Failed to apply templater to the note', userMessage: 'Failed to apply templater to the note', }); @@ -240,11 +240,11 @@ export class MediaDbFileHelper { return structuredClone(metadata ?? {}); } - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise> { const folder = options.folder ?? this.plugin.app.vault.getAbstractFileByPath('/'); if (!folder || !(folder instanceof TFolder)) { - return err({ kind: AppErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); + return err({ kind: MDBErrorKind.Validation, message: 'MDB | invalid folder', userMessage: 'MDB | invalid folder' }); } fileName = replaceIllegalFileNameCharactersInString(fileName); @@ -257,7 +257,7 @@ export class MediaDbFileHelper { }); if (!shouldOverwrite) { - return err({ kind: AppErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); + return err({ kind: MDBErrorKind.Cancelled, message: 'MDB | file creation cancelled by user', userMessage: 'MDB | file creation cancelled by user' }); } await this.plugin.app.fileManager.trashFile(file); @@ -278,7 +278,7 @@ export class MediaDbFileHelper { return ok(targetFile); } - private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { + private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise> { if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { const imageUrl = mediaTypeModel.image; const imageResult = await this.attempt( @@ -299,7 +299,7 @@ export class MediaDbFileHelper { mediaTypeModel.image = `[[${imagePath}]]`; }, { - kind: AppErrorKind.Network, + kind: MDBErrorKind.Network, message: 'MDB | Failed to download image', userMessage: 'Failed to download image', }, @@ -314,12 +314,12 @@ export class MediaDbFileHelper { return ok(undefined); } - private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { + private async attempt(operation: () => Promise | T, fallback: Omit): Promise> { return await Promise.resolve() .then(operation) .then( value => ok(value), - cause => err(toAppError(cause, fallback)), + cause => err(toMdbError(cause, fallback)), ); } } diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index 89a1b244..36f4b617 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -5,8 +5,8 @@ import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreview import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; -import type { AppError } from 'packages/obsidian/src/utils/AppError'; -import { AppErrorKind, toAppError } from 'packages/obsidian/src/utils/AppError'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Outcome } from 'packages/obsidian/src/utils/result'; import { cancelled, failure, OutcomeStatus, skipped, success } from 'packages/obsidian/src/utils/result'; @@ -106,7 +106,7 @@ export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { }; interface ModalCoreResult { - modalResult: Outcome; + modalResult: Outcome; modal: TModal; } @@ -119,10 +119,10 @@ export class ModalHelper { private async openModalCore( createModal: () => TModal, - wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, + wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, ): Promise> { const modal = createModal(); - const modalResult = await new Promise>(resolve => { + const modalResult = await new Promise>(resolve => { wireHandlers(modal, resolve); modal.open(); }); @@ -130,7 +130,7 @@ export class ModalHelper { return { modalResult, modal }; } - private async resolveOutcome(outcomePromise: Promise>): Promise { + private async resolveOutcome(outcomePromise: Promise>): Promise { const outcome = await outcomePromise; if (outcome.status === OutcomeStatus.Ok) { @@ -144,14 +144,14 @@ export class ModalHelper { return undefined; } - async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbSearchModal(this.plugin, searchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Search modal closed with an error' }))); return; } @@ -171,14 +171,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createSearchModalOutcome(searchModalOptions)); } - async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); return; } @@ -198,14 +198,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createAdvancedSearchModalOutcome(advancedSearchModalOptions)); } - async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { + async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Id search modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Id search modal closed with an error' }))); return; } @@ -225,7 +225,7 @@ export class ModalHelper { return await this.resolveOutcome(this.createIdSearchModalOutcome(idSearchModalOptions)); } - async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { + async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), (modal, resolve) => { @@ -233,7 +233,7 @@ export class ModalHelper { modal.setSkipCallback(() => resolve(skipped())); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Select modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Select modal closed with an error' }))); return; } @@ -253,14 +253,14 @@ export class ModalHelper { return await this.resolveOutcome(this.createSelectModalOutcome(selectModalOptions)); } - async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { + async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { const { modalResult, modal } = await this.openModalCore( () => new MediaDbPreviewModal(this.plugin, previewModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); modal.setCloseCb(err => { if (err) { - resolve(failure(toAppError(err, { kind: AppErrorKind.Modal, message: 'Preview modal closed with an error' }))); + resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Preview modal closed with an error' }))); return; } diff --git a/packages/obsidian/src/utils/result.ts b/packages/obsidian/src/utils/result.ts index 64c71c35..325a5f68 100644 --- a/packages/obsidian/src/utils/result.ts +++ b/packages/obsidian/src/utils/result.ts @@ -8,30 +8,44 @@ export interface Err { } export type Result = Ok | Err; -export const ok = (value: T): Ok => ({ ok: true, value }); -export const err = (error: E): Err => ({ ok: false, error }); +export function ok(value: T): Ok { + return { ok: true, value }; +} +export function err(error: E): Err { + return { ok: false, error }; +} -export const isOk = (result: Result): result is Ok => result.ok; -export const isErr = (result: Result): result is Err => !result.ok; +export function isOk(result: Result): result is Ok { + return result.ok; +} +export function isErr(result: Result): result is Err { + return !result.ok; +} -export const mapResult = (result: Result, mapper: (value: T) => U): Result => (result.ok ? ok(mapper(result.value)) : result); +export function mapResult(result: Result, mapper: (value: T) => U): Result { + return result.ok ? ok(mapper(result.value)) : result; +} -export const mapError = (result: Result, mapper: (error: E) => F): Result => (result.ok ? result : err(mapper(result.error))); +export function mapError(result: Result, mapper: (error: E) => F): Result { + return result.ok ? result : err(mapper(result.error)); +} -export const andThen = (result: Result, binder: (value: T) => Result): Result => (result.ok ? binder(result.value) : result); +export function andThen(result: Result, binder: (value: T) => Result): Result { + return result.ok ? binder(result.value) : result; +} -export const tapError = (result: Result, sideEffect: (error: E) => void): Result => { +export function tapError(result: Result, sideEffect: (error: E) => void): Result { if (!result.ok) sideEffect(result.error); return result; -}; +} -export const fromPromise = async (promise: Promise, onError: (cause: unknown) => E): Promise> => { +export async function fromPromise(promise: Promise, onError: (cause: unknown) => E): Promise> { try { return ok(await promise); } catch (cause) { return err(onError(cause)); } -}; +} export enum OutcomeStatus { Ok = 'ok', @@ -46,7 +60,15 @@ export type Outcome = | { status: OutcomeStatus.Skipped } | { status: OutcomeStatus.Error; error: E }; -export const cancelled = (): Outcome => ({ status: OutcomeStatus.Cancelled }); -export const skipped = (): Outcome => ({ status: OutcomeStatus.Skipped }); -export const success = (data: T): Outcome => ({ status: OutcomeStatus.Ok, data }); -export const failure = (error: E): Outcome => ({ status: OutcomeStatus.Error, error }); +export function cancelled(): Outcome { + return { status: OutcomeStatus.Cancelled }; +} +export function skipped(): Outcome { + return { status: OutcomeStatus.Skipped }; +} +export function success(data: T): Outcome { + return { status: OutcomeStatus.Ok, data }; +} +export function failure(error: E): Outcome { + return { status: OutcomeStatus.Error, error }; +} From 1405a0503fb7ed875bd2fedc7767dcc4806cc2b2 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 21 May 2026 20:04:33 +0200 Subject: [PATCH 20/35] more changes to modal flow, fixes some issues I hope --- .../src/modals/MediaDbAdvancedSearchModal.ts | 5 +- .../src/modals/MediaDbIdSearchModal.ts | 5 +- .../obsidian/src/modals/MediaDbSearchModal.ts | 5 +- packages/obsidian/src/styles.css | 27 ++++ .../obsidian/src/utils/MediaDbEntryHelper.ts | 56 ++++++-- packages/obsidian/src/utils/ModalHelper.ts | 132 ++++++++++++++---- test/modal-helper.test.ts | 113 +++++++++++++++ 7 files changed, 301 insertions(+), 42 deletions(-) create mode 100644 test/modal-helper.test.ts diff --git a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index 04f0598d..3bedff70 100644 --- a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -38,6 +38,8 @@ export class MediaDbAdvancedSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -57,8 +59,9 @@ export class MediaDbAdvancedSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, apis: apis }); } diff --git a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 1f65d30e..1427d1e8 100644 --- a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -38,6 +38,8 @@ export class MediaDbIdSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -55,8 +57,9 @@ export class MediaDbIdSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, api: this.selectedApi }); } diff --git a/packages/obsidian/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts index e62828d8..03398cb1 100644 --- a/packages/obsidian/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -41,6 +41,8 @@ export class MediaDbSearchModal extends Modal { keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { + event.preventDefault(); + event.stopImmediatePropagation(); void this.search(); } } @@ -60,8 +62,9 @@ export class MediaDbSearchModal extends Modal { if (!this.isBusy) { this.isBusy = true; - this.searchBtn?.setDisabled(false); + this.searchBtn?.setDisabled(true); this.searchBtn?.setButtonText('Searching...'); + this.searchBtn?.buttonEl.addClass('media-db-plugin-button-loading'); this.submitCallback?.({ query: this.query, types: types }); } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index 444af288..9cc1233a 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -69,6 +69,10 @@ small.media-db-plugin-list-text { } .media-db-plugin-search-input { + box-sizing: border-box; + display: block; + max-width: 100%; + min-width: 100%; width: 100%; } @@ -76,6 +80,29 @@ small.media-db-plugin-list-text { /*outline: 1px solid white;*/ } +.media-db-plugin-button-loading { + align-items: center; + display: inline-flex; + gap: var(--size-4-2); + justify-content: center; +} + +.media-db-plugin-button-loading::before { + animation: media-db-plugin-button-loading-spin 0.8s linear infinite; + border: 2px solid currentColor; + border-radius: 50%; + border-top-color: transparent; + content: ''; + height: 14px; + width: 14px; +} + +@keyframes media-db-plugin-button-loading-spin { + to { + transform: rotate(360deg); + } +} + .media-db-plugin-preview { border-radius: var(--modal-radius); border: var(--modal-border-width) solid var(--modal-border-color); diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 2f558d3a..7714161a 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -8,7 +8,9 @@ import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; -import type { SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import type { ModalLifecycle, ModalSession, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; +import type { Outcome } from 'packages/obsidian/src/utils/result'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; export class MediaDbEntryHelper { readonly plugin: MediaDbPlugin; @@ -21,13 +23,34 @@ export class MediaDbEntryHelper { this.plugin.errorReporter.report(error); } + private getModalData(modalResult: Outcome): T | undefined { + if (modalResult.status === OutcomeStatus.Ok) { + return modalResult.data; + } + + if (modalResult.status === OutcomeStatus.Error) { + this.reportMdbError(modalResult.error); + } + + return undefined; + } + + private async runModalQuery(session: ModalSession, query: () => Promise): Promise { + return this.getModalData(await this.plugin.modalHelper.runModalTask(session, query, { kind: MDBErrorKind.Api, message: 'API query failed' })); + } + async createLinkWithSearchModal(): Promise { - const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({}); + const advancedSearch = this.getModalData(advancedSearchSession.modalResult); if (!advancedSearch) { return; } - const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -57,14 +80,19 @@ export class MediaDbEntryHelper { } async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { - const searchData = await this.plugin.modalHelper.promptSearchModal(searchModalOptions ?? {}); + const searchSession = await this.plugin.modalHelper.createSearchModalSession(searchModalOptions ?? {}); + const searchData = this.getModalData(searchSession.modalResult); if (!searchData) { return; } const types = searchData.types; const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName); - const apiSearchResults = await this.plugin.apiManager.query(searchData.query, apis); + const apiSearchResults = await this.runModalQuery(searchSession, () => this.plugin.apiManager.query(searchData.query, apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -114,12 +142,17 @@ export class MediaDbEntryHelper { } async createEntryWithAdvancedSearchModal(): Promise { - const advancedSearch = await this.plugin.modalHelper.promptAdvancedSearchModal({}); + const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({}); + const advancedSearch = this.getModalData(advancedSearchSession.modalResult); if (!advancedSearch) { return; } - const apiSearchResults = await this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis); + const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis)); + if (!apiSearchResults) { + return; + } + if (!apiSearchResults.ok) { this.reportMdbError(apiSearchResults.error); return; @@ -153,12 +186,17 @@ export class MediaDbEntryHelper { let proceed = false; while (!proceed) { - const idSearchData = await this.plugin.modalHelper.promptIdSearchModal({}); + const idSearchSession = await this.plugin.modalHelper.createIdSearchModalSession({}); + const idSearchData = this.getModalData(idSearchSession.modalResult); if (!idSearchData) { return; } - const queriedIdResult = await this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api); + const queriedIdResult = await this.runModalQuery(idSearchSession, () => this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api)); + if (!queriedIdResult) { + return; + } + if (!queriedIdResult.ok) { this.reportMdbError(queriedIdResult.error); return; diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index 36f4b617..b014a8d3 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -105,9 +105,16 @@ export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { submitButtonText: 'Ok', }; -interface ModalCoreResult { +export interface ModalLifecycle { + open(): void; + close(): void; +} + +export interface ModalSession { modalResult: Outcome; modal: TModal; + close(): void; + isCancelled(): boolean; } export class ModalHelper { @@ -117,17 +124,39 @@ export class ModalHelper { this.plugin = plugin; } - private async openModalCore( + private async openModalCore( createModal: () => TModal, wireHandlers: (modal: TModal, resolve: (result: Outcome) => void) => void, - ): Promise> { + ): Promise> { const modal = createModal(); + let closeRequested = false; + let cancelledByUser = false; + let resolved = false; const modalResult = await new Promise>(resolve => { - wireHandlers(modal, resolve); + const resolveSession = (result: Outcome): void => { + if (result.status === OutcomeStatus.Cancelled && !closeRequested) { + cancelledByUser = true; + } + + if (!resolved) { + resolved = true; + resolve(result); + } + }; + + wireHandlers(modal, resolveSession); modal.open(); }); - return { modalResult, modal }; + return { + modalResult, + modal, + close: (): void => { + closeRequested = true; + modal.close(); + }, + isCancelled: (): boolean => cancelledByUser, + }; } private async resolveOutcome(outcomePromise: Promise>): Promise { @@ -144,8 +173,45 @@ export class ModalHelper { return undefined; } + async runModalTask( + session: ModalSession, + task: () => Promise, + errorFallback: MDBError = { kind: MDBErrorKind.Modal, message: 'Modal task failed' }, + ): Promise> { + try { + const result = await task(); + + if (session.isCancelled()) { + return cancelled(); + } + + return success(result); + } catch (err) { + if (session.isCancelled()) { + return cancelled(); + } + + return failure(toMdbError(err, errorFallback)); + } finally { + if (!session.isCancelled()) { + session.close(); + } + } + } + async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createSearchModalSession(searchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbSearchModal(this.plugin, searchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -159,12 +225,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptSearchModal(searchModalOptions: SearchModalOptions): Promise { @@ -172,7 +232,18 @@ export class ModalHelper { } async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -186,12 +257,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise { @@ -199,7 +264,18 @@ export class ModalHelper { } async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.createIdSearchModalSession(idSearchModalOptions); + const { modalResult } = session; + + if (modalResult.status === OutcomeStatus.Ok) { + session.close(); + } + + return modalResult; + } + + async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise> { + return await this.openModalCore( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -213,12 +289,6 @@ export class ModalHelper { }); }, ); - - if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); - } - - return modalResult; } async promptIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise { @@ -226,7 +296,7 @@ export class ModalHelper { } async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.openModalCore( () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -241,9 +311,10 @@ export class ModalHelper { }); }, ); + const { modalResult } = session; if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { - modal.close(); + session.close(); } return modalResult; @@ -254,7 +325,7 @@ export class ModalHelper { } async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { - const { modalResult, modal } = await this.openModalCore( + const session = await this.openModalCore( () => new MediaDbPreviewModal(this.plugin, previewModalOptions), (modal, resolve) => { modal.setSubmitCb(res => resolve(success(res))); @@ -268,9 +339,10 @@ export class ModalHelper { }); }, ); + const { modalResult } = session; if (modalResult.status === OutcomeStatus.Ok) { - modal.close(); + session.close(); } return modalResult; diff --git a/test/modal-helper.test.ts b/test/modal-helper.test.ts new file mode 100644 index 00000000..02d8f981 --- /dev/null +++ b/test/modal-helper.test.ts @@ -0,0 +1,113 @@ +import { expect, mock, test } from 'bun:test'; +import type MediaDbPlugin from 'packages/obsidian/src/main'; +import type { ModalLifecycle, ModalSession } from 'packages/obsidian/src/utils/ModalHelper'; +import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; +import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; +import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; + +mock.module('obsidian', () => ({ + AbstractInputSuggest: class {}, + Component: class { + load(): void {} + unload(): void {} + }, + DropdownComponent: class {}, + MarkdownRenderer: { render: async (): Promise => {} }, + MarkdownView: class {}, + Modal: class { + app: unknown; + + constructor(app: unknown) { + this.app = app; + } + + open(): void {} + close(): void {} + }, + Notice: class {}, + normalizePath: (path: string): string => path, + moment: Object.assign((value?: unknown): unknown => value, { locale: (): void => {} }), + parseYaml: (): unknown => ({}), + Plugin: class {}, + PluginSettingTab: class {}, + requestUrl: async (): Promise => ({}), + SecretComponent: class {}, + Setting: class {}, + SettingGroup: class {}, + stringifyYaml: (value: unknown): string => String(value), + TFile: class {}, + TFolder: class {}, + TextComponent: class {}, + ToggleComponent: class {}, +})); + +class FakeModal implements ModalLifecycle { + closeCount = 0; + + open(): void { + // Test modal does not need to render. + } + + close(): void { + this.closeCount += 1; + } +} + +function createModalSession(cancelled: () => boolean): ModalSession { + const modal = new FakeModal(); + + return { + modal, + modalResult: { status: OutcomeStatus.Ok, data: undefined }, + close: (): void => modal.close(), + isCancelled: cancelled, + }; +} + +async function createHelper(): Promise { + const { ModalHelper } = await import('packages/obsidian/src/utils/ModalHelper'); + return new ModalHelper({} as MediaDbPlugin); +} + +test('runModalTask closes the modal after active task success', async () => { + const helper = await createHelper(); + const session = createModalSession(() => false); + + const outcome = await helper.runModalTask(session, async () => 'done'); + + expect(outcome).toEqual({ status: OutcomeStatus.Ok, data: 'done' }); + expect(session.modal.closeCount).toBe(1); +}); + +test('runModalTask ignores late success after cancellation', async () => { + const helper = await createHelper(); + let cancelled = false; + const session = createModalSession(() => cancelled); + + const outcome = await helper.runModalTask(session, async () => { + cancelled = true; + return 'late result'; + }); + + expect(outcome).toEqual({ status: OutcomeStatus.Cancelled }); + expect(session.modal.closeCount).toBe(0); +}); + +test('runModalTask ignores late errors after cancellation', async () => { + const helper = await createHelper(); + let cancelled = false; + const session = createModalSession(() => cancelled); + const fallback: MDBError = { kind: MDBErrorKind.Modal, message: 'Task failed' }; + + const outcome = await helper.runModalTask( + session, + async () => { + cancelled = true; + throw new Error('Late failure'); + }, + fallback, + ); + + expect(outcome).toEqual({ status: OutcomeStatus.Cancelled }); + expect(session.modal.closeCount).toBe(0); +}); From 1bf7b477969144b16ccc8346781a4196da13407d Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 21 May 2026 20:14:11 +0200 Subject: [PATCH 21/35] another cleanup pass --- .../src/modals/MediaDbAdvancedSearchModal.ts | 6 +- .../src/modals/MediaDbIdSearchModal.ts | 6 +- .../obsidian/src/modals/MediaDbSearchModal.ts | 7 +- .../src/modals/MediaDbSearchResultModal.ts | 1 + .../src/modals/MediaDbSeasonSelectModal.ts | 7 +- packages/obsidian/src/modals/SelectModal.ts | 6 + .../obsidian/src/utils/MediaDbEntryHelper.ts | 15 +- packages/obsidian/src/utils/ModalHelper.ts | 201 +++++++++--------- 8 files changed, 125 insertions(+), 124 deletions(-) diff --git a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts index 3bedff70..2282376c 100644 --- a/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbAdvancedSearchModal.ts @@ -22,7 +22,7 @@ export class MediaDbAdvancedSearchModal extends Modal { super(plugin.app); this.plugin = plugin; - this.selectedApis = []; + this.selectedApis = [...(advancedSearchModalOptions.preselectedAPIs ?? [])]; this.title = advancedSearchModalOptions.modalTitle ?? ''; this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; @@ -101,7 +101,9 @@ export class MediaDbAdvancedSearchModal extends Modal { apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); apiToggleComponent.onChange(value => { if (value) { - this.selectedApis.push(api.apiName); + if (!this.selectedApis.includes(api.apiName)) { + this.selectedApis.push(api.apiName); + } } else { this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); } diff --git a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts index 1427d1e8..48a51923 100644 --- a/packages/obsidian/src/modals/MediaDbIdSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbIdSearchModal.ts @@ -23,8 +23,8 @@ export class MediaDbIdSearchModal extends Modal { this.plugin = plugin; this.title = idSearchModalOptions.modalTitle ?? ''; - this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; - this.query = ''; + this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0]?.apiName ?? ''; + this.query = idSearchModalOptions.prefilledSearchString ?? ''; this.isBusy = false; } @@ -74,6 +74,7 @@ export class MediaDbIdSearchModal extends Modal { const searchComponent = new TextComponent(contentEl); searchComponent.inputEl.addClass('media-db-plugin-search-input'); searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); searchComponent.onChange(value => (this.query = value)); searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); @@ -93,6 +94,7 @@ export class MediaDbIdSearchModal extends Modal { for (const api of this.plugin.apiManager.apis) { apiSelectorComponent.addOption(api.apiName, api.apiName); } + apiSelectorComponent.setValue(this.selectedApi); apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); diff --git a/packages/obsidian/src/modals/MediaDbSearchModal.ts b/packages/obsidian/src/modals/MediaDbSearchModal.ts index 03398cb1..248da599 100644 --- a/packages/obsidian/src/modals/MediaDbSearchModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchModal.ts @@ -109,12 +109,13 @@ export class MediaDbSearchModal extends Modal { if (value) { if (currentToggle && currentToggle !== apiToggleComponent) { currentToggle.setValue(false); - this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } currentToggle = apiToggleComponent; - this.selectedTypes.push(mediaType); + this.selectedTypes = [mediaType]; } else { - currentToggle = undefined; + if (currentToggle === apiToggleComponent) { + currentToggle = undefined; + } this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); } }); diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index 1cc035ff..4d7c4e14 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -140,5 +140,6 @@ export class MediaDbSearchResultModal extends SelectModal { onClose(): void { this.closeCallback?.(); + super.onClose(); } } diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index 4a72f09a..11132f06 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -5,6 +5,7 @@ import { MediaItemComponent } from 'packages/obsidian/src/Modals/MediaItemCompon export interface SeasonSelectModalElement { season_number: number; name: string; + episode_count?: number; air_date?: string; poster_path?: string; } @@ -40,7 +41,6 @@ export class MediaDbSeasonSelectModal extends SelectModal x.isActive()).map(x => x.value); this.submitCallback?.(selected); - this.close(); } skip(): void { @@ -54,4 +54,9 @@ export class MediaDbSeasonSelectModal extends SelectModal void): void { this.closeCallback = cb; } + + onClose(): void { + this.closeCallback?.(); + super.onClose(); + } } diff --git a/packages/obsidian/src/modals/SelectModal.ts b/packages/obsidian/src/modals/SelectModal.ts index 8d9ef3e6..d6c142fe 100644 --- a/packages/obsidian/src/modals/SelectModal.ts +++ b/packages/obsidian/src/modals/SelectModal.ts @@ -81,6 +81,7 @@ export abstract class SelectModal extends Modal { onOpen(): void { const { contentEl, titleEl } = this; + this.selectModalElements = []; titleEl.createEl('h2', { text: this.title }); contentEl.addClass('media-db-plugin-select-modal'); contentEl.createEl('p', { text: this.description }); @@ -125,6 +126,11 @@ export abstract class SelectModal extends Modal { }); } + onClose(): void { + this.selectModalElements = []; + this.contentEl.empty(); + } + activateHighlighted(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index 7714161a..b5c4f7e4 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -2,7 +2,6 @@ import { MarkdownView, Notice } from 'obsidian'; import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; @@ -288,22 +287,16 @@ export class MediaDbEntryHelper { } private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this.plugin, - allSeasons.map(season => ({ + return await this.plugin.modalHelper.promptSeasonSelectModal({ + seasons: allSeasons.map(season => ({ season_number: season.seasonNumber, name: season.seasonTitle || season.title, episode_count: season.episodes || 0, air_date: season.year, poster_path: season.image, })), - true, - seriesTitle, - ); - - return await new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); + multiSelect: true, + seriesName: seriesTitle, }); } diff --git a/packages/obsidian/src/utils/ModalHelper.ts b/packages/obsidian/src/utils/ModalHelper.ts index b014a8d3..3df0ee7a 100644 --- a/packages/obsidian/src/utils/ModalHelper.ts +++ b/packages/obsidian/src/utils/ModalHelper.ts @@ -4,6 +4,8 @@ import { MediaDbIdSearchModal } from 'packages/obsidian/src/modals/MediaDbIdSear import { MediaDbPreviewModal } from 'packages/obsidian/src/modals/MediaDbPreviewModal'; import { MediaDbSearchModal } from 'packages/obsidian/src/modals/MediaDbSearchModal'; import { MediaDbSearchResultModal } from 'packages/obsidian/src/modals/MediaDbSearchResultModal'; +import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; @@ -66,6 +68,12 @@ export interface PreviewModalOptions { elements?: MediaTypeModel[]; } +export interface SeasonSelectModalOptions { + seasons?: SeasonSelectModalElement[]; + multiSelect?: boolean; + seriesName?: string; +} + export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { modalTitle: 'Media DB Search', preselectedTypes: [], @@ -110,6 +118,14 @@ export interface ModalLifecycle { close(): void; } +interface CloseAwareModal extends ModalLifecycle { + setCloseCb(closeCallback: (err?: Error) => void): void; +} + +interface SubmitModal extends CloseAwareModal { + setSubmitCb(submitCallback: (res: TData) => void): void; +} + export interface ModalSession { modalResult: Outcome; modal: TModal; @@ -134,11 +150,11 @@ export class ModalHelper { let resolved = false; const modalResult = await new Promise>(resolve => { const resolveSession = (result: Outcome): void => { - if (result.status === OutcomeStatus.Cancelled && !closeRequested) { - cancelledByUser = true; - } - if (!resolved) { + if (result.status === OutcomeStatus.Cancelled && !closeRequested) { + cancelledByUser = true; + } + resolved = true; resolve(result); } @@ -159,6 +175,40 @@ export class ModalHelper { }; } + private createCloseOutcome(err: Error | undefined, message: string): Outcome { + if (err) { + return failure(toMdbError(err, { kind: MDBErrorKind.Modal, message })); + } + + return cancelled(); + } + + private async openSubmitModalSession>( + createModal: () => TModal, + closeErrorMessage: string, + wireExtraHandlers?: (modal: TModal, resolve: (result: Outcome) => void) => void, + ): Promise> { + return await this.openModalCore(createModal, (modal, resolve) => { + modal.setSubmitCb(res => resolve(success(res))); + modal.setCloseCb(err => resolve(this.createCloseOutcome(err, closeErrorMessage))); + wireExtraHandlers?.(modal, resolve); + }); + } + + private async resolveModalSessionOutcome( + sessionPromise: Promise>, + closeOn: readonly OutcomeStatus[] = [OutcomeStatus.Ok], + ): Promise> { + const session = await sessionPromise; + const { modalResult } = session; + + if (closeOn.includes(modalResult.status)) { + session.close(); + } + + return modalResult; + } + private async resolveOutcome(outcomePromise: Promise>): Promise { const outcome = await outcomePromise; @@ -200,30 +250,13 @@ export class ModalHelper { } async createSearchModalOutcome(searchModalOptions: SearchModalOptions): Promise> { - const session = await this.createSearchModalSession(searchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createSearchModalSession(searchModalOptions)); } async createSearchModalSession(searchModalOptions: SearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbSearchModal(this.plugin, searchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Search modal closed with an error', ); } @@ -232,30 +265,13 @@ export class ModalHelper { } async createAdvancedSearchModalOutcome(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - const session = await this.createAdvancedSearchModalSession(advancedSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createAdvancedSearchModalSession(advancedSearchModalOptions)); } async createAdvancedSearchModalSession(advancedSearchModalOptions: AdvancedSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Advanced search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Advanced search modal closed with an error', ); } @@ -264,30 +280,13 @@ export class ModalHelper { } async createIdSearchModalOutcome(idSearchModalOptions: IdSearchModalOptions): Promise> { - const session = await this.createIdSearchModalSession(idSearchModalOptions); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; + return await this.resolveModalSessionOutcome(this.createIdSearchModalSession(idSearchModalOptions)); } async createIdSearchModalSession(idSearchModalOptions: IdSearchModalOptions): Promise> { - return await this.openModalCore( + return await this.openSubmitModalSession( () => new MediaDbIdSearchModal(this.plugin, idSearchModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Id search modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + 'Id search modal closed with an error', ); } @@ -296,28 +295,16 @@ export class ModalHelper { } async createSelectModalOutcome(selectModalOptions: SelectModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setSkipCallback(() => resolve(skipped())); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Select modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbSearchResultModal(this.plugin, selectModalOptions), + 'Select modal closed with an error', + (modal, resolve) => { + modal.setSkipCallback(() => resolve(skipped())); + }, + ), + [OutcomeStatus.Ok, OutcomeStatus.Skipped], ); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok || modalResult.status === OutcomeStatus.Skipped) { - session.close(); - } - - return modalResult; } async promptSelectModal(selectModalOptions: SelectModalOptions): Promise { @@ -325,30 +312,34 @@ export class ModalHelper { } async createPreviewModalOutcome(previewModalOptions: PreviewModalOptions): Promise> { - const session = await this.openModalCore( - () => new MediaDbPreviewModal(this.plugin, previewModalOptions), - (modal, resolve) => { - modal.setSubmitCb(res => resolve(success(res))); - modal.setCloseCb(err => { - if (err) { - resolve(failure(toMdbError(err, { kind: MDBErrorKind.Modal, message: 'Preview modal closed with an error' }))); - return; - } - - resolve(cancelled()); - }); - }, + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => new MediaDbPreviewModal(this.plugin, previewModalOptions), + 'Preview modal closed with an error', + ), ); - const { modalResult } = session; - - if (modalResult.status === OutcomeStatus.Ok) { - session.close(); - } - - return modalResult; } async promptPreviewModal(previewModalOptions: PreviewModalOptions): Promise { return await this.resolveOutcome(this.createPreviewModalOutcome(previewModalOptions)); } + + async createSeasonSelectModalOutcome(seasonSelectModalOptions: SeasonSelectModalOptions): Promise> { + return await this.resolveModalSessionOutcome( + this.openSubmitModalSession( + () => + new MediaDbSeasonSelectModal( + this.plugin, + seasonSelectModalOptions.seasons ?? [], + seasonSelectModalOptions.multiSelect ?? true, + seasonSelectModalOptions.seriesName, + ), + 'Season select modal closed with an error', + ), + ); + } + + async promptSeasonSelectModal(seasonSelectModalOptions: SeasonSelectModalOptions): Promise { + return await this.resolveOutcome(this.createSeasonSelectModalOutcome(seasonSelectModalOptions)); + } } From 1f31acde88cd7d40e02ee233a3c72920f15ed42e Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:52:45 +0200 Subject: [PATCH 22/35] add repo config --- repo-automation.config.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 repo-automation.config.json diff --git a/repo-automation.config.json b/repo-automation.config.json new file mode 100644 index 00000000..2e5ed34e --- /dev/null +++ b/repo-automation.config.json @@ -0,0 +1,6 @@ +{ + "devBranch": "main", + "releaseBranch": "release", + "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin", + "preconditions": ["bun run typecheck", "bun run format", "bun run lint:fix", "bun run test"] +} From 2683ea7fc7d4beb6237e6544f1af6ee74c91402c Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:53:40 +0200 Subject: [PATCH 23/35] update deps --- bun.lock | 184 ++++++++++++++++++++++++++------------------------- package.json | 8 +-- 2 files changed, 97 insertions(+), 95 deletions(-) diff --git a/bun.lock b/bun.lock index 9f91cfa9..5d55e406 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", @@ -18,11 +18,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.12", + "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.10", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0", @@ -30,41 +30,41 @@ }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], @@ -130,7 +130,7 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], "@pkgr/core": ["@pkgr/core@0.1.2", "", {}, "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ=="], @@ -138,39 +138,39 @@ "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="], + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.15", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -184,19 +184,19 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -204,25 +204,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -260,23 +260,23 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -286,7 +286,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -330,11 +330,11 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.351", "", {}, "sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.365", "", {}, "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw=="], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], - "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -346,7 +346,7 @@ "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -364,7 +364,7 @@ "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "eslint-module-utils": ["eslint-module-utils@2.13.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ=="], "eslint-plugin-depend": ["eslint-plugin-depend@1.3.1", "", { "dependencies": { "empathic": "^2.0.0", "module-replacements": "^2.8.0", "semver": "^7.6.3" } }, "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw=="], @@ -468,7 +468,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -550,7 +550,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -624,7 +624,7 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -644,7 +644,7 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "obsidian": ["obsidian@1.13.0", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-PHw5+SAPlJ0S3leFvJ0wgFg63Z3DavxL6+d1ll+8toXR2ZlYKc1rMWqdUv9LgUbTwPQUyY6yfhOMMivampRRiQ=="], "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], @@ -682,7 +682,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -704,7 +704,7 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -712,7 +712,7 @@ "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], - "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -748,7 +748,7 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], @@ -782,7 +782,7 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -806,15 +806,15 @@ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], + "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -822,7 +822,7 @@ "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], - "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], @@ -844,15 +844,15 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "which-typed-array": ["which-typed-array@1.1.21", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], @@ -870,13 +870,15 @@ "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@redocly/openapi-core/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@redocly/openapi-core/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -888,13 +890,13 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "eslint-compat-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-compat-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "eslint-plugin-depend/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-depend/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -906,11 +908,11 @@ "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "eslint-plugin-n/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-n/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], - "eslint-plugin-obsidianmd/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "eslint-plugin-obsidianmd/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], @@ -920,7 +922,7 @@ "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "jsonc-eslint-parser/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "jsonc-eslint-parser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], @@ -934,17 +936,17 @@ "@microsoft/eslint-plugin-sdl/eslint-plugin-security/safe-regex": ["safe-regex@1.1.0", "", { "dependencies": { "ret": "~0.1.10" } }, "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "eslint-plugin-json-schema-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "eslint-plugin-json-schema-validator/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index 5661e62e..b743540a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "devDependencies": { "@happy-dom/global-registrator": "^20.9.0", "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", @@ -34,11 +34,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.12", + "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.58.0", - "vite": "^8.0.10", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0" From 254f7af6e9131607b90ad0ea0ba52ad56f30507e Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:54:13 +0200 Subject: [PATCH 24/35] fix config --- repo-automation.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo-automation.config.json b/repo-automation.config.json index 2e5ed34e..560b511f 100644 --- a/repo-automation.config.json +++ b/repo-automation.config.json @@ -1,5 +1,5 @@ { - "devBranch": "main", + "devBranch": "master", "releaseBranch": "release", "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin", "preconditions": ["bun run typecheck", "bun run format", "bun run lint:fix", "bun run test"] From 74e11d571b6bdc36f58e301bd4efbf0e153e00a4 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:55:48 +0200 Subject: [PATCH 25/35] fix errors --- bun.lock | 4 ++-- package.json | 2 +- packages/obsidian/src/main.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 5d55e406..614ffc27 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", @@ -380,7 +380,7 @@ "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], - "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.2.9", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-+dF5Zz5T6/j0QYGu+wHbY3UZb45Kh+QFkFdfvkVk05o4YIIVqHMlrTFrlRVhuBd6Htu8QxcFOwzeMTN4aysVTA=="], + "eslint-plugin-obsidianmd": ["eslint-plugin-obsidianmd@0.3.0", "", { "dependencies": { "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", "@microsoft/eslint-plugin-sdl": "^1.1.0", "@types/eslint": "9.6.1", "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", "eslint": ">=9.0.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-security": "2.1.1", "globals": "14.0.0", "obsidian": "1.12.3", "semver": "^7.7.4", "typescript": "5.4.5", "typescript-eslint": "^8.35.1" }, "bin": { "eslint-plugin-obsidian": "dist/lib/index.js" } }, "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w=="], "eslint-plugin-only-warn": ["eslint-plugin-only-warn@1.2.1", "", {}, "sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w=="], diff --git a/package.json b/package.json index b743540a..a2478227 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", - "eslint-plugin-obsidianmd": "^0.2.9", + "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", "iso-639-2": "^3.0.2", "obsidian": "latest", diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index 3bddc83c..48401d76 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -32,7 +32,7 @@ import { ModalHelper } from 'packages/obsidian/src/utils/ModalHelper'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; export default class MediaDbPlugin extends Plugin { - settings!: MediaDbPluginSettings; + declare settings: MediaDbPluginSettings; apiManager!: APIManager; mediaTypeManager!: MediaTypeManager; modelPropertyMapper!: PropertyMapper; From ad2bba4e9b33cc11f9b3ec5c140067e0fc206649 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 2 Jun 2026 13:56:06 +0200 Subject: [PATCH 26/35] [auto] bump version to `0.8.0-canary.20260602T115558` --- manifest-beta.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index de49a262..a4f1e1a2 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,8 +1,8 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB", - "version": "0.8.0-canary.20260406T152718", - "minAppVersion": "1.5.0", + "version": "0.8.0-canary.20260602T115558", + "minAppVersion": "1.12.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev", From f3ac8acf74ada12903f8c4c4acb96e25c18d7bd2 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 3 Jun 2026 19:06:03 +0200 Subject: [PATCH 27/35] remove solid; rework season search --- bun.lock | 133 ++------------- package.json | 3 - packages/obsidian/src/api/APIModel.ts | 15 ++ .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 7 +- .../obsidian/src/api/apis/TMDBSeriesAPI.ts | 4 + .../src/models/SeasonSearchResultModel.ts | 30 ++++ packages/obsidian/src/settings/Icon.tsx | 24 --- .../settings/PropertyMappingModelComponent.ts | 159 ++++++++++++++++++ .../PropertyMappingModelComponent.tsx | 138 --------------- .../PropertyMappingModelsComponent.ts | 40 +++++ .../PropertyMappingModelsComponent.tsx | 16 -- packages/obsidian/src/settings/Settings.ts | 45 ++--- packages/obsidian/src/styles.css | 7 + .../obsidian/src/utils/BulkImportHelper.ts | 29 +++- .../obsidian/src/utils/MediaDbEntryHelper.ts | 111 +++++++++--- tsconfig.json | 4 +- vite.config.mts | 2 - 17 files changed, 409 insertions(+), 358 deletions(-) create mode 100644 packages/obsidian/src/models/SeasonSearchResultModel.ts delete mode 100644 packages/obsidian/src/settings/Icon.tsx create mode 100644 packages/obsidian/src/settings/PropertyMappingModelComponent.ts delete mode 100644 packages/obsidian/src/settings/PropertyMappingModelComponent.tsx create mode 100644 packages/obsidian/src/settings/PropertyMappingModelsComponent.ts delete mode 100644 packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx diff --git a/bun.lock b/bun.lock index 614ffc27..e7ee5d42 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", "@types/bun": "^1.3.14", "eslint": "^9.39.4", - "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", @@ -18,13 +17,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0", }, }, @@ -32,40 +29,8 @@ "packages": { "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], @@ -110,16 +75,6 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lemons_dev/lemons-obsidian-plugin-automation": ["@lemons_dev/lemons-obsidian-plugin-automation@0.1.3", "", { "dependencies": { "@lemons_dev/parsinom": "^0.2.1", "moment": "^2.30.1", "string-argv": "^0.3.2" }, "peerDependencies": { "vite": "^8.0.9" }, "bin": { "lemons-automation": "bin/lemons-automation.js" } }, "sha512-Nv1eONy1Q9citS+nsMMasgL9ZfhKb57uinJmHRF+F1H+9HSiRkB5wlx9J/S1ou9cJL8IAhFvv72KGzE9HTIWtg=="], "@lemons_dev/parsinom": ["@lemons_dev/parsinom@0.2.1", "", {}, "sha512-ew+G3gm5aWJBnIhrysxI7k09FCAtAoj48wbTzr16NoX9ndhB8UnhmqjTxYVv46DsG88HQo+DTWPIFQW5qTAwwg=="], @@ -176,14 +131,6 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -196,7 +143,7 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], @@ -260,22 +207,14 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], - - "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -286,8 +225,6 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], @@ -302,14 +239,10 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -330,8 +263,6 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.365", "", {}, "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw=="], - "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], "enhanced-resolve": ["enhanced-resolve@5.22.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww=="], @@ -354,8 +285,6 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], @@ -434,8 +363,6 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -470,8 +397,6 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -536,8 +461,6 @@ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -552,8 +475,6 @@ "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-migrate": ["json-schema-migrate@2.0.0", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ=="], @@ -602,12 +523,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -624,8 +541,6 @@ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -666,8 +581,6 @@ "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -724,11 +637,7 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - - "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -748,10 +657,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], - - "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -814,9 +719,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -826,12 +729,8 @@ "vite-plugin-banner": ["vite-plugin-banner@0.8.1", "", {}, "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ=="], - "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.0", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ=="], - "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -850,8 +749,6 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], @@ -862,8 +759,6 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@microsoft/eslint-plugin-sdl/eslint-plugin-security": ["eslint-plugin-security@1.4.0", "", { "dependencies": { "safe-regex": "^1.1.0" } }, "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA=="], @@ -878,28 +773,22 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "eslint-compat-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "eslint-plugin-depend/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-json-schema-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "eslint-plugin-json-schema-validator/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], @@ -908,26 +797,24 @@ "eslint-plugin-n/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "eslint-plugin-n/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], - "eslint-plugin-obsidianmd/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "json-schema-migrate/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "jsonc-eslint-parser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "toml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -946,6 +833,8 @@ "eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index a2478227..ff7d109e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@lemons_dev/lemons-obsidian-plugin-automation": "^0.1.3", "@types/bun": "^1.3.14", "eslint": "^9.39.4", - "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-obsidianmd": "^0.3.0", "eslint-plugin-only-warn": "^1.2.1", @@ -34,13 +33,11 @@ "openapi-fetch": "^0.17.0", "openapi-typescript": "^7.13.0", "prettier": "^3.8.3", - "solid-js": "^1.9.13", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "vite": "^8.0.16", "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.12", "vite-plugin-static-copy": "^4.1.0" } } diff --git a/packages/obsidian/src/api/APIModel.ts b/packages/obsidian/src/api/APIModel.ts index c5bfe5d9..cc7808ba 100644 --- a/packages/obsidian/src/api/APIModel.ts +++ b/packages/obsidian/src/api/APIModel.ts @@ -1,9 +1,18 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import type { MediaType } from 'packages/obsidian/src/utils/MediaType'; import type { Result } from 'packages/obsidian/src/utils/result'; +export interface SeasonListAPIModel extends APIModel { + getSeasonsForSeries(seriesId: string): Promise>; +} + +export function isSeasonListAPIModel(api: APIModel | undefined): api is SeasonListAPIModel { + return typeof api?.getSeasonsForSeries === 'function'; +} + export abstract class APIModel { apiName!: string; apiUrl!: string; @@ -22,6 +31,12 @@ export abstract class APIModel { abstract getDisabledMediaTypes(): MediaType[]; + getSeasonsForSeries?(seriesId: string): Promise>; + + getSeasonApiNameForSeries(_series: MediaTypeModel): string | undefined { + return undefined; + } + hasType(type: MediaType): boolean { const disabledMediaTypes = this.getDisabledMediaTypes(); return this.types.includes(type) && !disabledMediaTypes.includes(type); diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts index fd6a37a7..44a61481 100644 --- a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -3,6 +3,7 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel'; import { Logger } from 'packages/obsidian/src/utils/Logger'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError'; @@ -153,15 +154,15 @@ export class TMDBSeasonAPI extends APIModel { } } - return new SeasonModel({ + return new SeasonSearchResultModel({ title: `${result.name ?? result.original_name ?? ''}`, englishTitle: result.name ?? result.original_name ?? '', year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', dataSource: this.apiName, id: result.id?.toString() ?? '', - seasonTitle: result.name ?? result.original_name ?? '', - seasonNumber: totalSeasons, + seasonCount: totalSeasons, image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', + }); }), ); diff --git a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts index 61f727fd..1a7f7af2 100644 --- a/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeriesAPI.ts @@ -252,4 +252,8 @@ export class TMDBSeriesAPI extends APIModel { getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes; } + + getSeasonApiNameForSeries(_series: MediaTypeModel): string { + return 'TMDBSeasonAPI'; + } } diff --git a/packages/obsidian/src/models/SeasonSearchResultModel.ts b/packages/obsidian/src/models/SeasonSearchResultModel.ts new file mode 100644 index 00000000..5d033386 --- /dev/null +++ b/packages/obsidian/src/models/SeasonSearchResultModel.ts @@ -0,0 +1,30 @@ +import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; +import { MediaType } from 'packages/obsidian/src/utils/MediaType'; +import type { ModelToData } from 'packages/obsidian/src/utils/Utils'; +import { migrateObject } from 'packages/obsidian/src/utils/Utils'; + +export type SeasonSearchResultData = ModelToData; + +export class SeasonSearchResultModel extends MediaTypeModel { + seasonCount: number; + + constructor(obj: SeasonSearchResultData) { + super(); + this.seasonCount = 0; + + migrateObject(this, obj, this); + this.type = this.getMediaType(); + } + + getTags(): string[] { + return []; + } + + getMediaType(): MediaType { + return MediaType.Season; + } + + getSummary(): string { + return `${this.seasonCount} ${this.seasonCount === 1 ? 'season' : 'seasons'}`; + } +} diff --git a/packages/obsidian/src/settings/Icon.tsx b/packages/obsidian/src/settings/Icon.tsx deleted file mode 100644 index 97231973..00000000 --- a/packages/obsidian/src/settings/Icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { onMount, Show } from 'solid-js'; -import { setIcon } from 'obsidian'; - -interface IconProps { - iconName?: string; -} - -export default function Icon(props: IconProps) { - let iconEl: HTMLDivElement | undefined; - - onMount(() => { - if (iconEl) { - setIcon(iconEl, props.iconName || ''); - } - }); - - return ( - 0}> -
-
-
-
- ); -} diff --git a/packages/obsidian/src/settings/PropertyMappingModelComponent.ts b/packages/obsidian/src/settings/PropertyMappingModelComponent.ts new file mode 100644 index 00000000..ac416585 --- /dev/null +++ b/packages/obsidian/src/settings/PropertyMappingModelComponent.ts @@ -0,0 +1,159 @@ +import { Component, setIcon } from 'obsidian'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from 'packages/obsidian/src/settings/PropertyMapping'; +import { capitalizeFirstLetter } from 'packages/obsidian/src/utils/Utils'; + +interface PropertyMappingModelComponentProps { + model: PropertyMappingModelData; + save: (model: PropertyMappingModelData) => void; +} + +export default class PropertyMappingModelComponent extends Component { + private readonly containerEl: HTMLElement; + private readonly save: (model: PropertyMappingModelData) => void; + private modelData: PropertyMappingModelData; + private unsavedChanges = false; + + constructor(containerEl: HTMLElement, props: PropertyMappingModelComponentProps) { + super(); + this.containerEl = containerEl; + this.modelData = props.model; + this.save = props.save; + } + + override onload(): void { + this.render(); + } + + override onunload(): void { + this.containerEl.empty(); + } + + private get validationResult(): { res: boolean; err?: Error } { + const model = PropertyMappingModel.fromJSON(this.modelData); + return model.validate(); + } + + private handleSave(): void { + const model = PropertyMappingModel.fromJSON(this.modelData); + if (!model.validate().res) { + return; + } + + this.save(model.toJSON()); + this.unsavedChanges = false; + this.render(); + } + + private onModelUpdate(): void { + this.unsavedChanges = true; + this.render(); + } + + private render(): void { + this.containerEl.empty(); + + const validationResult = this.validationResult; + const rootEl = this.containerEl.createDiv('media-db-plugin-property-mappings-model-container'); + const headerEl = rootEl.createDiv('media-db-plugin-property-mappings-model-header'); + headerEl.createDiv({ cls: 'setting-item-name', text: capitalizeFirstLetter(this.modelData.type) }); + + const actionsEl = headerEl.createDiv('media-db-plugin-property-mappings-model-actions'); + if (this.unsavedChanges) { + actionsEl.createDiv({ cls: 'media-db-plugin-property-mapping-unsaved-changes', text: 'Unsaved changes' }); + } + + const saveButtonEl = actionsEl.createEl('button', { + cls: `media-db-plugin-property-mappings-save-button ${validationResult.res ? 'mod-cta' : 'mod-muted'}`, + text: 'Save', + }); + saveButtonEl.addEventListener('click', () => this.handleSave()); + + if (!validationResult.res) { + rootEl.createDiv({ + cls: 'media-db-plugin-property-mapping-validation', + text: validationResult.err?.message ?? '', + }); + } + + const tableContainerEl = rootEl.createDiv('media-db-plugin-property-mappings-table-container'); + const tableEl = tableContainerEl.createEl('table', 'media-db-plugin-property-mappings-table'); + const tableHeadEl = tableEl.createEl('thead'); + const headerRowEl = tableHeadEl.createEl('tr'); + headerRowEl.createEl('th', { cls: 'col-property', text: 'Property' }); + headerRowEl.createEl('th', { cls: 'col-mapping', text: 'Mapping' }); + headerRowEl.createEl('th', { cls: 'col-new-name', text: 'New name' }); + headerRowEl.createEl('th', { cls: 'col-wikilink', text: 'Wikilink' }); + + const tableBodyEl = tableEl.createEl('tbody'); + for (const [index, property] of this.modelData.properties.entries()) { + const rowEl = tableBodyEl.createEl('tr'); + const propertyCellEl = rowEl.createEl('td', 'col-property'); + propertyCellEl.createEl('code', { text: property.property }); + + if (property.locked) { + const lockedCellEl = rowEl.createEl('td', 'col-locked'); + lockedCellEl.colSpan = 3; + lockedCellEl.createDiv({ + cls: 'media-db-plugin-property-binding-text', + text: 'property cannot be remapped', + }); + continue; + } + + const mappingCellEl = rowEl.createEl('td', 'col-mapping'); + const selectEl = mappingCellEl.createEl('select', 'dropdown'); + for (const remappingOption of propertyMappingOptions) { + selectEl.createEl('option', { + attr: { value: remappingOption }, + text: remappingOption, + }); + } + selectEl.value = propertyMappingOptions.includes(property.mapping) ? property.mapping : PropertyMappingOption.Default; + selectEl.addEventListener('change', event => { + const targetEl = event.currentTarget as HTMLSelectElement; + this.modelData.properties[index].mapping = targetEl.value as PropertyMappingOption; + this.modelData.properties[index].newProperty = ''; + this.onModelUpdate(); + }); + + const newNameCellEl = rowEl.createEl('td', 'col-new-name'); + if (property.mapping === PropertyMappingOption.Map) { + const mappingToEl = newNameCellEl.createDiv('media-db-plugin-property-mapping-to'); + const iconWrapperEl = mappingToEl.createDiv('icon-wrapper'); + const iconEl = iconWrapperEl.createDiv('icon'); + setIcon(iconEl, 'arrow-right'); + + const inputEl = mappingToEl.createEl('input', { + cls: 'media-db-plugin-property-mapping-input', + type: 'text', + }); + inputEl.spellcheck = false; + inputEl.value = property.newProperty; + inputEl.addEventListener('input', event => { + const targetEl = event.currentTarget as HTMLInputElement; + this.modelData.properties[index].newProperty = targetEl.value; + this.onModelUpdate(); + }); + } else { + newNameCellEl.createSpan({ + cls: 'media-db-plugin-property-mapping-to-disabled', + text: 'N/A', + }); + } + + const wikilinkCellEl = rowEl.createEl('td', 'col-wikilink'); + const wikilinkLabelEl = wikilinkCellEl.createEl('label', { + cls: 'media-db-plugin-property-mapping-wikilink-label', + attr: { title: 'Convert value to wikilink ([[value]])' }, + }); + const wikilinkInputEl = wikilinkLabelEl.createEl('input', { type: 'checkbox' }); + wikilinkInputEl.checked = property.wikilink ?? false; + wikilinkInputEl.addEventListener('change', event => { + const targetEl = event.currentTarget as HTMLInputElement; + this.modelData.properties[index].wikilink = targetEl.checked; + this.onModelUpdate(); + }); + } + } +} diff --git a/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx deleted file mode 100644 index c31bdbf9..00000000 --- a/packages/obsidian/src/settings/PropertyMappingModelComponent.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { createSignal, createMemo, For, Show } from 'solid-js'; -import { createStore } from 'solid-js/store'; -import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; -import { capitalizeFirstLetter } from '../utils/Utils'; -import Icon from './Icon'; - -interface PropertyMappingModelComponentProps { - model: PropertyMappingModelData; - save: (model: PropertyMappingModelData) => void; -} - -export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { - const [unsavedChanges, setUnsavedChanges] = createSignal(false); - - // Create a store from the model's plain data - const [modelData, setModelData] = createStore(props.model); - - // Derive the validation result reactively - const validationResult = createMemo(() => { - const model = PropertyMappingModel.fromJSON(modelData); - return model.validate(); - }); - - const onModelUpdate = () => { - setUnsavedChanges(true); - }; - - const handleSave = () => { - const model = PropertyMappingModel.fromJSON(modelData); - if (model.validate().res) { - props.save(model); - setUnsavedChanges(false); - } - }; - - return ( -
-
-
{capitalizeFirstLetter(modelData.type)}
- -
- -
Unsaved changes
-
- - -
-
- - -
{validationResult().err?.message}
-
- -
- - - - - - - - - - - - {(property, index) => ( - - - - -
property cannot be remapped
- - } - > -
- - - - - - - )} - - -
PropertyMappingNew nameWikilink
- {property.property} - - - - N/A} - > -
- - { - setModelData('properties', index(), 'newProperty', e.currentTarget.value); - onModelUpdate(); - }} - /> -
-
-
-
-
- ); -} diff --git a/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts b/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts new file mode 100644 index 00000000..269669d0 --- /dev/null +++ b/packages/obsidian/src/settings/PropertyMappingModelsComponent.ts @@ -0,0 +1,40 @@ +import { Component } from 'obsidian'; +import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping'; +import PropertyMappingModelComponent from 'packages/obsidian/src/settings/PropertyMappingModelComponent'; + +interface PropertyMappingModelsComponentProps { + models?: PropertyMappingModelData[]; + save: (model: PropertyMappingModelData) => void; +} + +export default class PropertyMappingModelsComponent extends Component { + private readonly containerEl: HTMLElement; + private readonly models: PropertyMappingModelData[]; + private readonly save: (model: PropertyMappingModelData) => void; + + constructor(containerEl: HTMLElement, props: PropertyMappingModelsComponentProps) { + super(); + this.containerEl = containerEl; + this.models = props.models ?? []; + this.save = props.save; + } + + override onload(): void { + this.containerEl.empty(); + const rootEl = this.containerEl.createDiv('setting-item media-db-plugin-property-mappings-models-container'); + + for (const model of this.models) { + const modelContainerEl = rootEl.createDiv(); + this.addChild( + new PropertyMappingModelComponent(modelContainerEl, { + model, + save: this.save, + }), + ); + } + } + + override onunload(): void { + this.containerEl.empty(); + } +} diff --git a/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx b/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx deleted file mode 100644 index 35274ff0..00000000 --- a/packages/obsidian/src/settings/PropertyMappingModelsComponent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { For } from 'solid-js'; -import { type PropertyMappingModelData } from './PropertyMapping'; -import PropertyMappingModelComponent from './PropertyMappingModelComponent'; - -interface PropertyMappingModelsComponentProps { - models?: PropertyMappingModelData[]; - save: (model: PropertyMappingModelData) => void; -} - -export default function PropertyMappingModelsComponent(props: PropertyMappingModelsComponentProps) { - return ( -
- {model => } -
- ); -} diff --git a/packages/obsidian/src/settings/Settings.ts b/packages/obsidian/src/settings/Settings.ts index 40ceb8d6..b0885f55 100644 --- a/packages/obsidian/src/settings/Settings.ts +++ b/packages/obsidian/src/settings/Settings.ts @@ -10,7 +10,6 @@ import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderS import { MediaType } from 'packages/obsidian/src/utils/MediaType'; import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager'; import { unCamelCase } from 'packages/obsidian/src/utils/Utils'; -import { render } from 'solid-js/web'; function createDateFormatDescription(preview: string): DocumentFragment { return createFragment(frag => { @@ -425,14 +424,25 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; + private propertyMappingModelsComponent?: PropertyMappingModelsComponent; constructor(app: App, plugin: MediaDbPlugin) { super(app, plugin); this.plugin = plugin; } + override hide(): void { + this.propertyMappingModelsComponent?.unload(); + this.propertyMappingModelsComponent = undefined; + super.hide(); + } + display(): void { const { containerEl } = this; + if (this.propertyMappingModelsComponent) { + this.propertyMappingModelsComponent.unload(); + this.propertyMappingModelsComponent = undefined; + } containerEl.empty(); const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); @@ -837,24 +847,21 @@ export class MediaDbSettingTab extends PluginSettingTab { mappingGroup.setHeading('Property mappings'); mappingGroup.addSetting(setting => { setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription()); - - render( - () => - PropertyMappingModelsComponent({ - models: structuredClone(this.plugin.settings.propertyMappingModels), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }), - setting.descEl, - ); + const propertyMappingsEl = setting.descEl.createDiv(); + this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, { + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }); + this.propertyMappingModelsComponent.load(); }); } } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index 9cc1233a..f079ecce 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -124,6 +124,13 @@ small.media-db-plugin-list-text { } /* Property Mapping Component Styles */ +.media-db-plugin-property-mappings-models-container { + display: flex; + gap: 10px; + flex-direction: column; + align-items: stretch; +} + .media-db-plugin-property-mappings-model-container { margin-bottom: var(--size-4-8); } diff --git a/packages/obsidian/src/utils/BulkImportHelper.ts b/packages/obsidian/src/utils/BulkImportHelper.ts index f91ae374..cd1d535b 100644 --- a/packages/obsidian/src/utils/BulkImportHelper.ts +++ b/packages/obsidian/src/utils/BulkImportHelper.ts @@ -1,7 +1,7 @@ import type { TFolder } from 'obsidian'; import { TFile } from 'obsidian'; import type MediaDbPlugin from 'packages/obsidian/src/main'; -import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; +import { MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import { OutcomeStatus } from 'packages/obsidian/src/utils/result'; import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils'; @@ -26,7 +26,7 @@ export class BulkImportHelper { async import(folder: TFolder): Promise { const erroredFiles: BulkImportError[] = []; - let canceled: boolean = false; + let canceled = false; const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ selectedAPI: string; @@ -87,7 +87,11 @@ export class BulkImportHelper { } if (modelResult.value) { - await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + const createResult = await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined); + if (!createResult.ok) { + return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message }; + } + return undefined; } @@ -127,8 +131,25 @@ export class BulkImportHelper { return { filePath: file.path, error: `no search results selected` }; } + const seasonSelectionResult = await this.plugin.entryHelper.handleSeasonSearchSelections(selectModalResult.data.selected, appendContent ? file : undefined); + if (seasonSelectionResult.handled) { + if (!seasonSelectionResult.created) { + return { filePath: file.path, error: 'season selection canceled or no season note was created' }; + } + + return undefined; + } + const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected); - await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + if (detailedResults.length === 0) { + return { filePath: file.path, error: 'failed to load details for selected search results' }; + } + + const createResult = await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + if (!createResult.ok) { + return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message }; + } + return undefined; } diff --git a/packages/obsidian/src/utils/MediaDbEntryHelper.ts b/packages/obsidian/src/utils/MediaDbEntryHelper.ts index b5c4f7e4..428c465d 100644 --- a/packages/obsidian/src/utils/MediaDbEntryHelper.ts +++ b/packages/obsidian/src/utils/MediaDbEntryHelper.ts @@ -1,9 +1,12 @@ +import type { TFile } from 'obsidian'; import { MarkdownView, Notice } from 'obsidian'; -import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI'; +import type { SeasonListAPIModel } from 'packages/obsidian/src/api/APIModel'; +import { isSeasonListAPIModel } from 'packages/obsidian/src/api/APIModel'; import type MediaDbPlugin from 'packages/obsidian/src/main'; import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel'; +import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel'; import type { MDBError } from 'packages/obsidian/src/utils/MDBError'; import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError'; import { MediaType } from 'packages/obsidian/src/utils/MediaType'; @@ -112,6 +115,7 @@ export class MediaDbEntryHelper { types.length === 1 && types[0] === MediaType.Season ? await this.plugin.modalHelper.promptSelectModal({ elements: filteredSearchResults, + multiSelect: false, description: 'Select one search result to proceed.', submitButtonText: 'Ok', }) @@ -121,7 +125,11 @@ export class MediaDbEntryHelper { return; } - const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected); + if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) { + return; + } + + const selectResults = await this.queryDetails(selectResultsData.selected); if (selectResults.length === 0) { return; @@ -167,6 +175,10 @@ export class MediaDbEntryHelper { return; } + if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) { + return; + } + const selectResults = await this.queryDetails(selectResultsData.selected); if (selectResults.length < 1) { return; @@ -239,31 +251,62 @@ export class MediaDbEntryHelper { return detailModels; } - private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { - if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { - const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); - return { handled: true, seasonsCreated: created }; + private async handleSeasonWorkflow(types: MediaType[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { + if (!types.includes(MediaType.Series) || !types.includes(MediaType.Season)) { + return { handled: false }; } - if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) { - const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI'); - if (seriesResults.length === 1 && types.includes('season')) { - const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); - return { handled: true, seasonsCreated: created }; - } + const seriesResults = selectResults.filter(result => result.getMediaType() === MediaType.Series); + if (seriesResults.length !== 1) { + return { handled: false }; + } + + const seriesResult = seriesResults[0]; + const sourceApi = this.plugin.apiManager.getApiByName(seriesResult.dataSource); + const seasonApiName = sourceApi?.getSeasonApiNameForSeries(seriesResult); + if (seasonApiName) { + const created = await this.showSeasonSelectAndCreate(seriesResult.id, seriesResult.title, undefined, seasonApiName); + return { handled: true, seasonsCreated: created }; } return { handled: false }; } - private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { - const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not available.'); + private isSeasonSearchResult(model: MediaTypeModel): model is SeasonSearchResultModel { + return model instanceof SeasonSearchResultModel || (model.getMediaType() === MediaType.Season && typeof (model as { seasonCount?: unknown }).seasonCount === 'number'); + } + + async handleSeasonSearchSelections(selectedResults: MediaTypeModel[], attachFile?: TFile): Promise<{ handled: boolean; created: boolean }> { + const seasonSearchResults = selectedResults.filter(result => this.isSeasonSearchResult(result)); + if (seasonSearchResults.length === 0) { + return { handled: false, created: false }; + } + + if (selectedResults.length !== 1) { + new Notice('Select exactly one season search result before choosing seasons.'); + return { handled: true, created: false }; + } + + const selectedResult = seasonSearchResults[0]; + return { + handled: true, + created: await this.showSeasonSelectAndCreate(selectedResult.id, selectedResult.englishTitle || selectedResult.title, attachFile, selectedResult.dataSource), + }; + } + + private getSeasonListApi(apiName: string): SeasonListAPIModel | undefined { + const api = this.plugin.apiManager.getApiByName(apiName); + return isSeasonListAPIModel(api) ? api : undefined; + } + + private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string, attachFile: TFile | undefined, seasonApiName: string): Promise { + const seasonAPI = this.getSeasonListApi(seasonApiName); + if (!seasonAPI) { + new Notice(`${seasonApiName} does not support season selection.`); return false; } - const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); + const allSeasonsResult = await seasonAPI.getSeasonsForSeries(seriesId); if (!allSeasonsResult.ok) { this.reportMdbError(allSeasonsResult.error); new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`); @@ -281,8 +324,13 @@ export class MediaDbEntryHelper { return false; } - await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); - new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); + const createdCount = await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, seasonAPI, attachFile); + if (createdCount === 0) { + new Notice('No season entries were created.'); + return false; + } + + new Notice(`Successfully created ${createdCount} season ${createdCount === 1 ? 'entry' : 'entries'}.`); return true; } @@ -300,21 +348,36 @@ export class MediaDbEntryHelper { }); } - private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { - await Promise.all( + private async createNotesForSelectedSeasons( + selectedSeasons: SeasonSelectModalElement[], + allSeasons: SeasonModel[], + seasonAPI: SeasonListAPIModel, + attachFile?: TFile, + ): Promise { + const results = await Promise.all( selectedSeasons.map(async selectedSeason => { const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number); if (seasonModel) { - const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id); + const fullMetadataResult = await seasonAPI.getById(seasonModel.id); if (!fullMetadataResult.ok) { this.reportMdbError(fullMetadataResult.error); new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`); - return; + return false; } - await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]); + const createResult = await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value], attachFile); + if (!createResult.ok) { + this.reportMdbError(createResult.error); + return false; + } + + return true; } + + return false; }), ); + + return results.filter(created => created).length; } } diff --git a/tsconfig.json b/tsconfig.json index 7ce937bb..bb7ef48b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,9 +23,7 @@ "sourceMap": true, "lib": ["DOM", "ESNext"], "allowSyntheticDefaultImports": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", "types": ["vite/client"] }, - "include": ["packages/obsidian/**/*.ts", "packages/obsidian/**/*.tsx"] + "include": ["packages/obsidian/**/*.ts"] } diff --git a/vite.config.mts b/vite.config.mts index e42ec6d9..2cf072fa 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,5 +1,4 @@ import { UserConfig, defineConfig } from 'vite'; -import solid from 'vite-plugin-solid'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; import path from 'path'; @@ -14,7 +13,6 @@ export default defineConfig(async ({ mode }) => { const outDir = prod ? 'dist/' : `exampleVault/.obsidian/plugins/${manifest.id}/`; let plugins = [ - solid(), banner({ outDir: outDir, content: getBuildBanner(prod ? 'Release Build' : 'Dev Build', version => version), From 27b320ed1e50432700d5838f13fb8014fc5fc367 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Wed, 3 Jun 2026 19:09:38 +0200 Subject: [PATCH 28/35] add check pr workflow --- .github/workflows/checkPR.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/checkPR.yml diff --git a/.github/workflows/checkPR.yml b/.github/workflows/checkPR.yml new file mode 100644 index 00000000..96fb0463 --- /dev/null +++ b/.github/workflows/checkPR.yml @@ -0,0 +1,25 @@ +name: Run Checks and Tests on PR + +on: pull_request + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install Dependencies + id: build + run: | + bun install + + - name: Run Checks + run: | + bun run check \ No newline at end of file From 213268dd2bf27cd1a315728a6529ce507b2386c6 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:27:39 +0200 Subject: [PATCH 29/35] Fix casing + ran prettier --- .github/workflows/checkPR.yml | 2 +- .../obsidian/src/api/apis/TMDBSeasonAPI.ts | 1 - .../src/modals/MediaDbSearchResultModal.ts | 21 ++++++++++++------- .../src/modals/MediaDbSeasonSelectModal.ts | 2 +- .../obsidian/src/modals/MediaItemComponent.ts | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/checkPR.yml b/.github/workflows/checkPR.yml index 96fb0463..82fd5b70 100644 --- a/.github/workflows/checkPR.yml +++ b/.github/workflows/checkPR.yml @@ -22,4 +22,4 @@ jobs: - name: Run Checks run: | - bun run check \ No newline at end of file + bun run check diff --git a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts index 44a61481..7fd6bb8e 100644 --- a/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts +++ b/packages/obsidian/src/api/apis/TMDBSeasonAPI.ts @@ -162,7 +162,6 @@ export class TMDBSeasonAPI extends APIModel { id: result.id?.toString() ?? '', seasonCount: totalSeasons, image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', - }); }), ); diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index 4d7c4e14..6d6eaf1e 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -1,10 +1,9 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaItemComponent } from 'packages/obsidian/src/modals/MediaItemComponent'; import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel'; import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper'; -import { MediaItemComponent } from 'packages/obsidian/src/Modals/MediaItemComponent'; - export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; @@ -70,8 +69,8 @@ export class MediaDbSearchResultModal extends SelectModal { }); // Store references for later updates - (item as any).__titleEl = titleEl; - (item as any).__summaryEl = summaryEl; + (item as unknown).__titleEl = titleEl; + (item as unknown).__summaryEl = summaryEl; }, }); @@ -98,12 +97,18 @@ export class MediaDbSearchResultModal extends SelectModal { console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`); - setTimeout(async () => { + window.setTimeout(async () => { if (item.image && item.year) return; console.debug('MDB | auto-fetching detail for', item.dataSource, item.id); try { console.debug('MDB | fetching detailed info for', item.dataSource, item.id); - const detailed = await this.plugin.apiManager.queryDetailedInfo(item); + const detailedResult = await this.plugin.apiManager.queryDetailedInfo(item); + if (!detailedResult.ok) { + console.warn('MDB | failed to fetch detailed info', detailedResult.error); + return; + } + + const detailed = detailedResult.value; console.debug('MDB | detailed fetch result', detailed?.dataSource, detailed?.id, detailed?.image, detailed?.year); if (detailed?.image && !item.image) { @@ -113,8 +118,8 @@ export class MediaDbSearchResultModal extends SelectModal { if (!item.year && detailed?.year) { item.year = detailed.year; - const titleEl = (item as any).__titleEl; - const summaryEl = (item as any).__summaryEl; + const titleEl = (item as unknown).__titleEl; + const summaryEl = (item as unknown).__summaryEl; if (titleEl) titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); if (summaryEl) summaryEl.textContent = `${item.getSummary()}\n`; } diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index 11132f06..b3bac27b 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -1,6 +1,6 @@ import type MediaDbPlugin from 'packages/obsidian/src/main'; +import { MediaItemComponent } from 'packages/obsidian/src/modals/MediaItemComponent'; import { SelectModal } from 'packages/obsidian/src/modals/SelectModal'; -import { MediaItemComponent } from 'packages/obsidian/src/Modals/MediaItemComponent'; export interface SeasonSelectModalElement { season_number: number; diff --git a/packages/obsidian/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts index b2d668ee..81cafff9 100644 --- a/packages/obsidian/src/modals/MediaItemComponent.ts +++ b/packages/obsidian/src/modals/MediaItemComponent.ts @@ -16,8 +16,8 @@ export class MediaItemComponent { constructor(container: HTMLElement, options: MediaItemComponentOptions) { this.container = container; this.options = options; - this.thumbEl = null as any; // Will be initialized in setup - this.contentEl = null as any; + this.thumbEl = null as unknown; // Will be initialized in setup + this.contentEl = null as unknown; this.setup(); } From 176bf25a537aa9b4242e9fb6923327aa0b8597f2 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:33:36 +0200 Subject: [PATCH 30/35] Fixed error when image url can't be downloaded --- packages/obsidian/src/utils/MediaDbFileHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/obsidian/src/utils/MediaDbFileHelper.ts b/packages/obsidian/src/utils/MediaDbFileHelper.ts index 4c7db6f2..d2a509da 100644 --- a/packages/obsidian/src/utils/MediaDbFileHelper.ts +++ b/packages/obsidian/src/utils/MediaDbFileHelper.ts @@ -307,7 +307,8 @@ export class MediaDbFileHelper { if (!imageResult.ok) { Logger.warn('MDB | Failed to download image:', imageResult.error); - return imageResult; + delete mediaTypeModel.image; + return ok(undefined); } } From 7e32ea411c7f32db0478e47e88601fe357d0db69 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:35:26 +0200 Subject: [PATCH 31/35] Improve rate limit delay with auto-fetcher Also increased MAL API delay from 400ms to 750ms to avoid rate limits --- .../src/modals/MediaDbSearchResultModal.ts | 27 ++++++++++--------- .../src/modals/MediaDbSeasonSelectModal.ts | 2 +- .../obsidian/src/modals/MediaItemComponent.ts | 6 ++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index 6d6eaf1e..2bb1629e 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -16,6 +16,7 @@ export class MediaDbSearchResultModal extends SelectModal { skipCallback?: () => void; submitButtonText: string; + private autoFetchIndex: number; constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions); super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect); @@ -26,6 +27,7 @@ export class MediaDbSearchResultModal extends SelectModal { this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok'; this.busy = false; this.sendCallback = false; + this.autoFetchIndex = 0; } setSubmitCb(submitCallback: (res: SelectModalData) => void): void { @@ -40,10 +42,10 @@ export class MediaDbSearchResultModal extends SelectModal { this.skipCallback = skipCallback; } - // Different rate limit delay based on API source, MAL APIs = max 3 per second so 400ms between requests to be safe + // Different rate limit delay based on API source, MAL APIs = max 3 per second but that still seems to trigger rate limits, so using 750ms delay private getDelayForApi(dataSource: string): number { const isMalApi = dataSource === 'MALAPI' || dataSource === 'MALAPIManga'; - return isMalApi ? 400 : 200; + return isMalApi ? 750 : 200; } // Renders each suggestion item. @@ -52,13 +54,13 @@ export class MediaDbSearchResultModal extends SelectModal { const mediaComponent = new MediaItemComponent(el, { imageUrl: this.getImageUrl(item), imageAlt: item.title, - onImageError: () => { + onImageError: (): void => { console.debug('MDB | Image failed to load for', item.id); }, - onImageLoad: () => { + onImageLoad: (): void => { console.debug('MDB | Image loaded for', item.id); }, - renderContent: contentEl => { + renderContent: (contentEl: HTMLElement): void => { const titleEl = contentEl.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title', @@ -68,9 +70,9 @@ export class MediaDbSearchResultModal extends SelectModal { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`, }); - // Store references for later updates - (item as unknown).__titleEl = titleEl; - (item as unknown).__summaryEl = summaryEl; + const typedItem = item as MediaTypeModel & { __titleEl?: HTMLElement; __summaryEl?: HTMLElement }; + typedItem.__titleEl = titleEl; + typedItem.__summaryEl = summaryEl; }, }); @@ -92,8 +94,8 @@ export class MediaDbSearchResultModal extends SelectModal { if (!needsFetch) return; const apiDelay = this.getDelayForApi(item.dataSource); - const element = document.getElementById(`media-db-plugin-select-element-${this.selectModalElements.length}`); - const delayMs = element ? (parseInt(element.id.split('-').pop() ?? '0') ?? 0) * apiDelay : 0; + const index = this.autoFetchIndex++; + const delayMs = index * apiDelay; console.debug('MDB | will auto-fetch detail for', item.dataSource, item.id, 'in', delayMs, 'ms', `(${apiDelay}ms per request)`); @@ -118,8 +120,9 @@ export class MediaDbSearchResultModal extends SelectModal { if (!item.year && detailed?.year) { item.year = detailed.year; - const titleEl = (item as unknown).__titleEl; - const summaryEl = (item as unknown).__summaryEl; + const typedItem = item as MediaTypeModel & { __titleEl?: HTMLElement; __summaryEl?: HTMLElement }; + const titleEl = typedItem.__titleEl; + const summaryEl = typedItem.__summaryEl; if (titleEl) titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); if (summaryEl) summaryEl.textContent = `${item.getSummary()}\n`; } diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index b3bac27b..a8a5e047 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -29,7 +29,7 @@ export class MediaDbSeasonSelectModal extends SelectModal { + renderContent: (contentEl: HTMLElement): void => { contentEl.createEl('div', { text: `${season.name}` }); if (season.air_date) { contentEl.createEl('small', { text: `Air date: ${season.air_date}` }); diff --git a/packages/obsidian/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts index 81cafff9..5402a2cf 100644 --- a/packages/obsidian/src/modals/MediaItemComponent.ts +++ b/packages/obsidian/src/modals/MediaItemComponent.ts @@ -8,16 +8,14 @@ export interface MediaItemComponentOptions { export class MediaItemComponent { private container: HTMLElement; - private thumbEl: HTMLElement; - private contentEl: HTMLElement; + private thumbEl!: HTMLElement; + private contentEl!: HTMLElement; private imgEl: HTMLImageElement | undefined; private options: MediaItemComponentOptions; constructor(container: HTMLElement, options: MediaItemComponentOptions) { this.container = container; this.options = options; - this.thumbEl = null as unknown; // Will be initialized in setup - this.contentEl = null as unknown; this.setup(); } From 01945b069e054dc8246e437abe57aec2eb0e1a31 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 03:29:23 +0200 Subject: [PATCH 32/35] Move css to styles.css (WIP) --- .../obsidian/src/modals/MediaItemComponent.ts | 24 ++----------- packages/obsidian/src/styles.css | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/obsidian/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts index 5402a2cf..1ec4f55c 100644 --- a/packages/obsidian/src/modals/MediaItemComponent.ts +++ b/packages/obsidian/src/modals/MediaItemComponent.ts @@ -23,26 +23,12 @@ export class MediaItemComponent { private setup(): void { // Set container layout this.container.addClass('media-item-component'); - this.container.style.display = 'flex'; - this.container.style.gap = '8px'; - this.container.style.alignItems = 'flex-start'; // Create thumbnail this.thumbEl = this.container.createDiv({ cls: 'media-item-thumb' }); - this.thumbEl.style.width = '48px'; - this.thumbEl.style.height = '72px'; - this.thumbEl.style.flex = '0 0 48px'; - this.thumbEl.style.overflow = 'hidden'; - this.thumbEl.style.background = 'var(--background-modifier-hover)'; - this.thumbEl.style.borderRadius = '4px'; - this.thumbEl.style.display = 'flex'; - this.thumbEl.style.alignItems = 'center'; - this.thumbEl.style.justifyContent = 'center'; // Create content area this.contentEl = this.container.createDiv({ cls: 'media-item-content' }); - this.contentEl.style.flex = '1'; - this.contentEl.style.minWidth = '0'; // Render custom content this.options.renderContent(this.contentEl); @@ -61,10 +47,6 @@ export class MediaItemComponent { this.imgEl.loading = 'lazy'; this.imgEl.alt = this.options.imageAlt || 'Media item'; this.imgEl.className = 'media-item-image'; - this.imgEl.style.width = '100%'; - this.imgEl.style.height = '100%'; - this.imgEl.style.objectFit = 'cover'; - this.imgEl.style.display = 'block'; this.imgEl.onerror = () => { this.showPlaceholder(); @@ -84,9 +66,7 @@ export class MediaItemComponent { private showPlaceholder(): void { this.thumbEl.empty(); - const span = this.thumbEl.createEl('span', { text: '📷' }); - span.style.fontSize = '24px'; - span.style.color = 'var(--text-muted)'; + this.thumbEl.createEl('span', { text: '📷', cls: 'media-item-placeholder' }); } public updateImage(url: string | undefined): void { @@ -98,7 +78,7 @@ export class MediaItemComponent { } } else if (url === 'NSFW') { this.thumbEl.empty(); - this.thumbEl.createEl('span', { text: 'NSFW' }); + this.thumbEl.createEl('span', { text: 'NSFW', cls: 'media-item-placeholder' }); } else { this.showPlaceholder(); } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index f079ecce..106fc8c7 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -41,6 +41,41 @@ small.media-db-plugin-list-text { font-size: 16px; } +.media-item-component { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.media-item-thumb { + width: 48px; + height: 72px; + flex: 0 0 48px; + overflow: hidden; + background: var(--background-modifier-hover); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.media-item-content { + flex: 1; + min-width: 0; +} + +.media-item-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.media-item-placeholder { + font-size: 24px; + color: var(--text-muted); +} + .media-db-plugin-select-title { font-weight: 600; } From 417e2ed5f68b7c879e595f6bd97329ccc2b35735 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:49:04 +0200 Subject: [PATCH 33/35] normalized css classes + added missing return type on function --- .../obsidian/src/modals/MediaItemComponent.ts | 20 +++++++++---------- packages/obsidian/src/styles.css | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/obsidian/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts index 1ec4f55c..b89e1af3 100644 --- a/packages/obsidian/src/modals/MediaItemComponent.ts +++ b/packages/obsidian/src/modals/MediaItemComponent.ts @@ -22,13 +22,13 @@ export class MediaItemComponent { private setup(): void { // Set container layout - this.container.addClass('media-item-component'); + this.container.addClass('media-db-plugin-select-media-item-component'); // Create thumbnail - this.thumbEl = this.container.createDiv({ cls: 'media-item-thumb' }); + this.thumbEl = this.container.createDiv({ cls: 'media-db-plugin-select-media-item-thumb' }); // Create content area - this.contentEl = this.container.createDiv({ cls: 'media-item-content' }); + this.contentEl = this.container.createDiv({ cls: 'media-db-plugin-select-media-item-content' }); // Render custom content this.options.renderContent(this.contentEl); @@ -43,17 +43,17 @@ export class MediaItemComponent { private loadImage(url: string): void { if (!this.imgEl) { - this.imgEl = document.createElement('img'); + this.imgEl = activeDocument.createElement('img'); this.imgEl.loading = 'lazy'; - this.imgEl.alt = this.options.imageAlt || 'Media item'; - this.imgEl.className = 'media-item-image'; + this.imgEl.alt = this.options.imageAlt ?? 'Media item'; + this.imgEl.className = 'media-db-plugin-select-media-item-image'; - this.imgEl.onerror = () => { + this.imgEl.onerror = (): void => { this.showPlaceholder(); this.options.onImageError?.(); }; - this.imgEl.onload = () => { + this.imgEl.onload = (): void => { this.options.onImageLoad?.(); }; @@ -66,7 +66,7 @@ export class MediaItemComponent { private showPlaceholder(): void { this.thumbEl.empty(); - this.thumbEl.createEl('span', { text: '📷', cls: 'media-item-placeholder' }); + this.thumbEl.createEl('span', { text: '📷', cls: 'media-db-plugin-select-media-item-placeholder' }); } public updateImage(url: string | undefined): void { @@ -78,7 +78,7 @@ export class MediaItemComponent { } } else if (url === 'NSFW') { this.thumbEl.empty(); - this.thumbEl.createEl('span', { text: 'NSFW', cls: 'media-item-placeholder' }); + this.thumbEl.createEl('span', { text: 'NSFW', cls: 'media-db-plugin-select-media-item-placeholder' }); } else { this.showPlaceholder(); } diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index 106fc8c7..60ed5e1a 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -41,13 +41,13 @@ small.media-db-plugin-list-text { font-size: 16px; } -.media-item-component { +.media-db-plugin-select-media-item-component { display: flex; gap: 8px; align-items: flex-start; } -.media-item-thumb { +.media-db-plugin-select-media-item-thumb { width: 48px; height: 72px; flex: 0 0 48px; @@ -59,19 +59,19 @@ small.media-db-plugin-list-text { justify-content: center; } -.media-item-content { +.media-db-plugin-select-media-item-content { flex: 1; min-width: 0; } -.media-item-image { +.media-db-plugin-select-media-item-image { width: 100%; height: 100%; object-fit: cover; display: block; } -.media-item-placeholder { +.media-db-plugin-select-media-item-placeholder { font-size: 24px; color: var(--text-muted); } From 6a4913a71835225d1eaed982809a3b08c2858e76 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:50:25 +0200 Subject: [PATCH 34/35] Replace emoji with setIcon --- packages/obsidian/src/modals/MediaItemComponent.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/obsidian/src/modals/MediaItemComponent.ts b/packages/obsidian/src/modals/MediaItemComponent.ts index b89e1af3..3c887373 100644 --- a/packages/obsidian/src/modals/MediaItemComponent.ts +++ b/packages/obsidian/src/modals/MediaItemComponent.ts @@ -1,3 +1,5 @@ +import { setIcon } from 'obsidian'; + export interface MediaItemComponentOptions { imageUrl?: string; imageAlt?: string; @@ -66,7 +68,9 @@ export class MediaItemComponent { private showPlaceholder(): void { this.thumbEl.empty(); - this.thumbEl.createEl('span', { text: '📷', cls: 'media-db-plugin-select-media-item-placeholder' }); + const iconEl = this.thumbEl.createDiv('icon'); + setIcon(iconEl, 'image'); + iconEl.className = 'media-db-plugin-select-media-item-placeholder'; } public updateImage(url: string | undefined): void { From 1cfeca5f10345fa93170b74eb001607bcf8536ec Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:53:12 +0200 Subject: [PATCH 35/35] Several small improvements * remove hacky double underscore * added doc comments annotating functions * removed unneeded comments * use .createDiv( instead of createEl('div', --- .../src/modals/MediaDbSearchResultModal.ts | 43 +++++++++++++------ .../src/modals/MediaDbSeasonSelectModal.ts | 2 +- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts index 2bb1629e..ad7e4434 100644 --- a/packages/obsidian/src/modals/MediaDbSearchResultModal.ts +++ b/packages/obsidian/src/modals/MediaDbSearchResultModal.ts @@ -5,6 +5,11 @@ import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel import type { SelectModalData, SelectModalOptions } from 'packages/obsidian/src/utils/ModalHelper'; import { SELECTMODALOPTIONSDEFAULT } from 'packages/obsidian/src/utils/ModalHelper'; +interface RenderedMediaItem extends MediaTypeModel { + titleEl?: HTMLElement; + summaryEl?: HTMLElement; +} + export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; @@ -17,6 +22,7 @@ export class MediaDbSearchResultModal extends SelectModal { submitButtonText: string; private autoFetchIndex: number; + constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions); super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect); @@ -42,15 +48,23 @@ export class MediaDbSearchResultModal extends SelectModal { this.skipCallback = skipCallback; } - // Different rate limit delay based on API source, MAL APIs = max 3 per second but that still seems to trigger rate limits, so using 750ms delay + /** + * Returns the rate limit delay based on API source. MAL APIs allow max 3 per second, but that still triggers rate limits, so using 750ms delay. + * @param dataSource The API source name (e.g., 'MALAPI', 'MALAPIManga') + * @returns The delay in milliseconds (750ms for MAL, 200ms for others) + */ private getDelayForApi(dataSource: string): number { const isMalApi = dataSource === 'MALAPI' || dataSource === 'MALAPIManga'; return isMalApi ? 750 : 200; } - // Renders each suggestion item. + /** + * Renders a media suggestion item in the modal. + * Creates the MediaItemComponent and auto-fetches detailed info if the item lacks image or year. + * @param item The media type model to render + * @param el The HTMLElement to render into + */ renderElement(item: MediaTypeModel, el: HTMLElement): void { - // Create the media item component const mediaComponent = new MediaItemComponent(el, { imageUrl: this.getImageUrl(item), imageAlt: item.title, @@ -61,7 +75,7 @@ export class MediaDbSearchResultModal extends SelectModal { console.debug('MDB | Image loaded for', item.id); }, renderContent: (contentEl: HTMLElement): void => { - const titleEl = contentEl.createEl('div', { + const titleEl = contentEl.createDiv({ text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title', }); @@ -70,13 +84,12 @@ export class MediaDbSearchResultModal extends SelectModal { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`, }); - const typedItem = item as MediaTypeModel & { __titleEl?: HTMLElement; __summaryEl?: HTMLElement }; - typedItem.__titleEl = titleEl; - typedItem.__summaryEl = summaryEl; + const renderedItem: RenderedMediaItem = item; + renderedItem.titleEl = titleEl; + renderedItem.summaryEl = summaryEl; }, }); - // Auto-fetch detailed info if needed this.autoFetchDetails(item, mediaComponent); } @@ -120,11 +133,14 @@ export class MediaDbSearchResultModal extends SelectModal { if (!item.year && detailed?.year) { item.year = detailed.year; - const typedItem = item as MediaTypeModel & { __titleEl?: HTMLElement; __summaryEl?: HTMLElement }; - const titleEl = typedItem.__titleEl; - const summaryEl = typedItem.__summaryEl; - if (titleEl) titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); - if (summaryEl) summaryEl.textContent = `${item.getSummary()}\n`; + + const renderedItem: RenderedMediaItem = item; + if (renderedItem.titleEl) { + renderedItem.titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); + } + if (renderedItem.summaryEl) { + renderedItem.summaryEl.textContent = `${item.getSummary()}\n`; + } } } catch (e) { console.warn('MDB | Failed to fetch detail', e); @@ -132,7 +148,6 @@ export class MediaDbSearchResultModal extends SelectModal { }, delayMs); } - // Perform action on the selected suggestion. submit(): void { if (!this.busy) { this.busy = true; diff --git a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts index a8a5e047..09f6b4f8 100644 --- a/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts +++ b/packages/obsidian/src/modals/MediaDbSeasonSelectModal.ts @@ -30,7 +30,7 @@ export class MediaDbSeasonSelectModal extends SelectModal { - contentEl.createEl('div', { text: `${season.name}` }); + contentEl.createDiv({ text: `${season.name}` }); if (season.air_date) { contentEl.createEl('small', { text: `Air date: ${season.air_date}` }); }