refactor season search code

This commit is contained in:
Moritz Jung 2026-01-29 20:02:45 +01:00
parent 418601d2d1
commit faeca3459c

View file

@ -22,6 +22,7 @@ import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal';
import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal';
import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal';
import type { MediaTypeModel } from './models/MediaTypeModel'; import type { MediaTypeModel } from './models/MediaTypeModel';
import type { SeasonModel } from './models/SeasonModel';
import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMapper } from './settings/PropertyMapper';
import { PropertyMappingModel } from './settings/PropertyMapping'; import { PropertyMappingModel } from './settings/PropertyMapping';
import type { MediaDbPluginSettings } from './settings/Settings'; import type { MediaDbPluginSettings } from './settings/Settings';
@ -203,125 +204,163 @@ export default class MediaDbPlugin extends Plugin {
types = searchModalData.types; types = searchModalData.types;
const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName);
try { try {
console.log(apis);
return await this.apiManager.query(searchModalData.query, apis); return await this.apiManager.query(searchModalData.query, apis);
} catch (e) { } catch (e) {
console.warn(e); console.warn('MDB | Query failed:', e);
new Notice(`Search failed: ${e}`);
return []; return [];
} }
}); });
if (!apiSearchResults) { if (!apiSearchResults || apiSearchResults.length === 0) {
// TODO: add new notice saying no results found? new Notice('No results found.');
return; return;
} }
// filter the results // filter the results
apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); apiSearchResults = apiSearchResults.filter(x => types.contains(x.type));
let selectResults: MediaTypeModel[]; if (apiSearchResults.length === 0) {
const proceed: boolean = false; new Notice('No results found for the selected types.');
return;
}
while (!proceed) { // Show selection modal - for seasons, skip detail query
if (types.length === 1 && types[0] === 'season') { const selectResults =
selectResults = types.length === 1 && types[0] === 'season'
(await this.modalHelper.openSelectModal( ? await this.modalHelper.openSelectModal(
{ {
elements: apiSearchResults, elements: apiSearchResults,
description: 'Select one search result to proceed.', description: 'Select one search result to proceed.',
submitButtonText: 'Ok', submitButtonText: 'Ok',
}, },
async selectModalData => { async selectModalData => selectModalData.selected,
return selectModalData.selected; )
}, : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected));
)) ?? [];
} else {
selectResults =
(await this.modalHelper.openSelectModal(
{
elements: apiSearchResults,
},
async selectModalData => {
return await this.queryDetails(selectModalData.selected);
},
)) ?? [];
}
if (!selectResults || selectResults.length < 1) {
return;
}
// Only show the season select modal if the user searches for seasons if (!selectResults || selectResults.length === 0) {
if (await this.handleSeasonSelectModal(types, selectResults)) { return;
return;
}
const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
return previewModalData.confirmed;
});
if (!confirmed) {
return;
}
break;
} }
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<boolean> { * 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') { if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') {
// Use static import for the modal const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title);
const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; return { handled: true, seasonsCreated: created };
if (!tmdbSeasonAPI) { }
new Notice('TMDBSeasonAPI not found.');
return true; // 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<boolean> {
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 // 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) { if (!allSeasons || allSeasons.length === 0) {
new Notice('No seasons found for this series.'); 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 || ''; // Show season selection modal
const modal = new MediaDbSeasonSelectModal( const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle);
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();
});
if (!selectedSeasons || selectedSeasons.length === 0) { if (!selectedSeasons || selectedSeasons.length === 0) {
return true; return false;
} }
// Fetch full metadata for each selected season and create the note
await Promise.all( // Create notes for all selected seasons in parallel
selectedSeasons.map(async season => { await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI);
const orig = allSeasons.find(s => s.seasonNumber === season.season_number); new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`);
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]);
}
}
}),
);
return true; 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<SeasonSelectModalElement[] | undefined> {
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<void> {
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<void> { async createEntryWithAdvancedSearchModal(): Promise<void> {
@ -329,8 +368,8 @@ export default class MediaDbPlugin extends Plugin {
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);
}); });
if (!apiSearchResults) { if (!apiSearchResults || apiSearchResults.length === 0) {
// TODO: add new notice saying no results found? new Notice('No results found.');
return; return;
} }
@ -382,19 +421,32 @@ export default class MediaDbPlugin extends Plugin {
} }
async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise<void> { async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise<void> {
for (const model of models) { // Create notes in parallel for better performance
await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); 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<MediaTypeModel[]> { async queryDetails(models: MediaTypeModel[]): Promise<MediaTypeModel[]> {
const detailModels: MediaTypeModel[] = []; // Query details in parallel for better performance
for (const model of models) { const results = await Promise.allSettled(models.map(model => this.apiManager.queryDetailedInfo(model)));
const res = await this.apiManager.queryDetailedInfo(model);
if (res) { // Filter out failures and return successful results
detailModels.push(res); const detailModels: MediaTypeModel[] = results
} .filter((r): r is PromiseFulfilledResult<MediaTypeModel | undefined> => 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; return detailModels;
} }