From 18af97a4c687bd088913971cef49f7f9b0ec20bd Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:30:12 +0200 Subject: [PATCH] Added season selection modal to improve TMDB season handling You can now search for a series, select it and it'll list all the seasons for that series. With the new modal you can then select the seasons you want to add. --- src/api/apis/TMDBSeasonAPI.ts | 109 +++++++++++++------------ src/main.ts | 61 +++++++++++++- src/modals/MediaDbSeasonSelectModal.ts | 47 +++++++++++ src/models/SeasonModel.ts | 2 +- src/utils/SeasonModalHelper.ts | 16 ++++ 5 files changed, 178 insertions(+), 57 deletions(-) create mode 100644 src/modals/MediaDbSeasonSelectModal.ts create mode 100644 src/utils/SeasonModalHelper.ts diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 4db5105..d0ea90f 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -48,65 +48,70 @@ export class TMDBSeasonAPI extends APIModel { for (const result of searchData.results) { if (ret.length >= 20) break; - const tvId = result.id; - const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`; - const seriesResp = await fetch(seriesUrl); - - if (seriesResp.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (seriesResp.status !== 200) { - console.warn(`MDB | Skipping series ${tvId} due to status ${seriesResp.status}`); - continue; - } - - const seriesData = await seriesResp.json(); - const seriesName = seriesData?.name ?? result?.name ?? result?.original_name ?? ''; - - if (Array.isArray(seriesData?.seasons)) { - for (const season of seriesData.seasons) { - if (ret.length >= 20) break; - - const seasonNumber = season.season_number ?? 0; - const seasonDetailsUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}/season/${encodeURIComponent(seasonNumber)}?api_key=${this.plugin.settings.TMDBKey}`; - const seasonDetailsResp = await fetch(seasonDetailsUrl); - - if (seasonDetailsResp.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + // Fetch series details to get the total number of seasons + let totalSeasons = 0; + try { + const detailsUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(result.id)}?api_key=${this.plugin.settings.TMDBKey}`; + const detailsResp = await fetch(detailsUrl); + if (detailsResp.status === 200) { + const detailsData = await detailsResp.json(); + if (Array.isArray(detailsData.seasons)) { + totalSeasons = detailsData.seasons.length; } - if (seasonDetailsResp.status !== 200) { - console.warn(`MDB | Skipping season ${tvId}/season/${seasonNumber} due to status ${seasonDetailsResp.status}`); - continue; - } - - const seasonData = await seasonDetailsResp.json(); - - // 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 titleText = `${seriesName} - Season ${seasonNumber}`; - ret.push( - new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: seasonData.air_date ? new Date(seasonData.air_date).getFullYear().toString() : 'unknown', - dataSource: this.apiName, - id: `${tvId}/season/${seasonNumber}`, - seasonTitle: seasonData.name ?? titleText, - seasonNumber: seasonNumber, - }), - ); } - } + } catch {} + 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', + dataSource: this.apiName, + id: result.id.toString(), + seasonTitle: result.name ?? result.original_name ?? '', + seasonNumber: totalSeasons, + }) + ); } return ret; } + //Fetch all seasons for a given series + async getSeasonsForSeries(tvId: string): Promise { + if (!this.plugin.settings.TMDBKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}`; + const seriesResp = await fetch(seriesUrl); + if (seriesResp.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (seriesResp.status !== 200) { + throw Error(`MDB | Received status code ${seriesResp.status} from ${this.apiName}.`); + } + const seriesData = await seriesResp.json(); + 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, + }) + ); + } + } + return ret; + } + async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); diff --git a/src/main.ts b/src/main.ts index 1fcadc5..ffb6ed7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -218,14 +218,67 @@ export default class MediaDbPlugin extends Plugin { const proceed: boolean = false; while (!proceed) { - selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; + if (types.length === 1 && types[0] === 'season') { + selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, 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; } + // Only show the season select modal if the user searches for seasons + if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { + // Dynamically import the modal + const { MediaDbSeasonSelectModal } = await import('./modals/MediaDbSeasonSelectModal'); + const TMDBSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as import('./api/apis/TMDBSeasonAPI').TMDBSeasonAPI; + if (!TMDBSeasonAPI) { + new Notice('TMDBSeasonAPI not found.'); + return; + } + // Fetch all seasons for the selected series + const allSeasons = await TMDBSeasonAPI.getSeasonsForSeries(selectResults[0].id); + if (!allSeasons || allSeasons.length === 0) { + new Notice('No seasons found for this series.'); + return; + } + 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); + const selectedSeasons: any[] = await new Promise(resolve => { + modal.setSubmitCallback(resolve); + modal.open(); + }); + if (!selectedSeasons || selectedSeasons.length === 0) { + return; + } + // Fetch full metadata for each selected seasond 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 import('./api/apis/TMDBSeasonAPI').TMDBSeasonAPI; + if (TMDBSeasonAPI) { + const fullMeta = await TMDBSeasonAPI.getById(orig.id); + await this.createMediaDbNotes([fullMeta]); + } else { + await this.createMediaDbNotes([orig]); + } + } + })); + return; + } + const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { return previewModalData.confirmed; }); diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts new file mode 100644 index 0000000..2490e67 --- /dev/null +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -0,0 +1,47 @@ +import type MediaDbPlugin from '../main'; +import { SelectModal } from './SelectModal'; + +export interface SeasonSelectModalElement { + season_number: number; + name: string; + air_date?: string; + poster_path?: string; +} + +export class MediaDbSeasonSelectModal extends SelectModal { + plugin: MediaDbPlugin; + submitCallback?: (selectedSeasons: SeasonSelectModalElement[]) => void; + closeCallback?: (err?: Error) => void; + + constructor(plugin: MediaDbPlugin, seasons: SeasonSelectModalElement[], multiSelect = true) { + super(plugin.app, seasons, multiSelect); + this.plugin = plugin; + this.title = 'Select Season(s)'; + this.description = 'Select one or more seasons to create notes for.'; + } + + renderElement(season: SeasonSelectModalElement, el: HTMLElement): void { + el.createEl('div', { text: `${season.name}` }); + if (season.air_date) { + el.createEl('small', { text: `Air date: ${season.air_date}` }); + } + } + + submit(): void { + const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value); + this.submitCallback?.(selected); + this.close(); + } + + skip(): void { + this.close(); + } + + setSubmitCallback(cb: (selectedSeasons: SeasonSelectModalElement[]) => void): void { + this.submitCallback = cb; + } + + setCloseCallback(cb: (err?: Error) => void): void { + this.closeCallback = cb; + } +} diff --git a/src/models/SeasonModel.ts b/src/models/SeasonModel.ts index ef5a20a..07f9626 100644 --- a/src/models/SeasonModel.ts +++ b/src/models/SeasonModel.ts @@ -75,6 +75,6 @@ export class SeasonModel extends MediaTypeModel { } getSummary(): string { - return 'Season ' + this.seasonNumber + '(' + this.year + ')'; + return this.seasonNumber + ' seasons'; } } diff --git a/src/utils/SeasonModalHelper.ts b/src/utils/SeasonModalHelper.ts new file mode 100644 index 0000000..93002df --- /dev/null +++ b/src/utils/SeasonModalHelper.ts @@ -0,0 +1,16 @@ +import type { App } from 'obsidian'; +import type { SeasonSelectModalElement } from '../modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from '../modals/MediaDbSeasonSelectModal'; + +export async function openSeasonSelectModal(app: App, plugin: any, seasons: SeasonSelectModalElement[]): Promise { + return new Promise(resolve => { + const modal = new MediaDbSeasonSelectModal(plugin, seasons, true); + modal.setSubmitCallback(selected => { + resolve(selected); + }); + modal.setCloseCallback(() => { + resolve(undefined); + }); + modal.open(); + }); +}