From faeca3459c2fbb1ae90d80775c7cb34628420b86 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 29 Jan 2026 20:02:45 +0100 Subject: [PATCH] refactor season search code --- src/main.ts | 244 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 148 insertions(+), 96 deletions(-) diff --git a/src/main.ts b/src/main.ts index ce08a21..df97525 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,6 +22,7 @@ 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'; @@ -203,125 +204,163 @@ export default class MediaDbPlugin extends Plugin { types = searchModalData.types; const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); try { - console.log(apis); return await this.apiManager.query(searchModalData.query, apis); } catch (e) { - console.warn(e); + console.warn('MDB | Query failed:', e); + new Notice(`Search failed: ${e}`); return []; } }); - if (!apiSearchResults) { - // TODO: add new notice saying no results found? + if (!apiSearchResults || apiSearchResults.length === 0) { + new Notice('No results found.'); return; } // filter the results apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); - let selectResults: MediaTypeModel[]; - const proceed: boolean = false; + if (apiSearchResults.length === 0) { + new Notice('No results found for the selected types.'); + return; + } - while (!proceed) { - if (types.length === 1 && types[0] === 'season') { - selectResults = - (await this.modalHelper.openSelectModal( + // 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 => { - return selectModalData.selected; - }, - )) ?? []; - } else { - selectResults = - (await this.modalHelper.openSelectModal( - { - elements: apiSearchResults, - }, - async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }, - )) ?? []; - } - if (!selectResults || selectResults.length < 1) { - return; - } + async selectModalData => selectModalData.selected, + ) + : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected)); - // Only show the season select modal if the user searches for seasons - if (await this.handleSeasonSelectModal(types, selectResults)) { - return; - } - - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - break; + if (!selectResults || selectResults.length === 0) { + return; } - await this.createMediaDbNotes(selectResults!); + // 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); } - // Season select modal - private async handleSeasonSelectModal(types: string[], selectResults: MediaTypeModel[]): Promise { + /** + * 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') { - // Use static import for the modal - const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not found.'); - return true; + 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(selectResults[0].id); + const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); if (!allSeasons || allSeasons.length === 0) { new Notice('No seasons found for this series.'); - return true; + return false; } - // Pass the original series title from the search result - const seriesName = selectResults[0]?.englishTitle || selectResults[0]?.title || ''; - 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, - seriesName, - ); - const selectedSeasons: SeasonSelectModalElement[] = await new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); - }); + + // Show season selection modal + const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); if (!selectedSeasons || selectedSeasons.length === 0) { - return true; + return false; } - // Fetch full metadata for each selected season and create the note - await Promise.all( - selectedSeasons.map(async season => { - const orig = allSeasons.find(s => s.seasonNumber === season.season_number); - if (orig) { - // Fetch full metadata using getById - const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; - if (tmdbSeasonAPI) { - const fullMeta = await tmdbSeasonAPI.getById(orig.id); - await this.createMediaDbNotes([fullMeta]); - } else { - await this.createMediaDbNotes([orig]); - } - } - }), - ); + + // 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; } - 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 { @@ -329,8 +368,8 @@ export default class MediaDbPlugin extends Plugin { return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); }); - if (!apiSearchResults) { - // TODO: add new notice saying no results found? + if (!apiSearchResults || apiSearchResults.length === 0) { + new Notice('No results found.'); return; } @@ -382,19 +421,32 @@ export default class MediaDbPlugin extends Plugin { } async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - for (const model of models) { - await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); + // 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 { - const detailModels: MediaTypeModel[] = []; - for (const model of models) { - const res = await this.apiManager.queryDetailedInfo(model); - if (res) { - detailModels.push(res); - } + // 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; }