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 786df40a..71806a80 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -39,11 +39,116 @@ 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.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.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`; + }; + + 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); + } + } } // Perform action on the selected suggestion. diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index 0066c31e..f5d4586b 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -24,9 +24,45 @@ 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}` }); } } diff --git a/src/styles.css b/src/styles.css index b204d1d6..b532da6a 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: 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);