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 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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;