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.
This commit is contained in:
parent
0e729e59d3
commit
18af97a4c6
5 changed files with 178 additions and 57 deletions
|
|
@ -48,62 +48,67 @@ export class TMDBSeasonAPI extends APIModel {
|
||||||
for (const result of searchData.results) {
|
for (const result of searchData.results) {
|
||||||
if (ret.length >= 20) break;
|
if (ret.length >= 20) break;
|
||||||
|
|
||||||
const tvId = result.id;
|
// Fetch series details to get the total number of seasons
|
||||||
const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`;
|
let totalSeasons = 0;
|
||||||
const seriesResp = await fetch(seriesUrl);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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<SeasonModel[]> {
|
||||||
|
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) {
|
if (seriesResp.status === 401) {
|
||||||
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`);
|
||||||
}
|
}
|
||||||
if (seriesResp.status !== 200) {
|
if (seriesResp.status !== 200) {
|
||||||
console.warn(`MDB | Skipping series ${tvId} due to status ${seriesResp.status}`);
|
throw Error(`MDB | Received status code ${seriesResp.status} from ${this.apiName}.`);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesData = await seriesResp.json();
|
const seriesData = await seriesResp.json();
|
||||||
const seriesName = seriesData?.name ?? result?.name ?? result?.original_name ?? '';
|
const seriesName = seriesData?.name ?? '';
|
||||||
|
const ret: SeasonModel[] = [];
|
||||||
if (Array.isArray(seriesData?.seasons)) {
|
if (Array.isArray(seriesData?.seasons)) {
|
||||||
for (const season of seriesData.seasons) {
|
for (const season of seriesData.seasons) {
|
||||||
if (ret.length >= 20) break;
|
|
||||||
|
|
||||||
const seasonNumber = season.season_number ?? 0;
|
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.`);
|
|
||||||
}
|
|
||||||
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}`;
|
const titleText = `${seriesName} - Season ${seasonNumber}`;
|
||||||
ret.push(
|
ret.push(
|
||||||
new SeasonModel({
|
new SeasonModel({
|
||||||
title: titleText,
|
title: titleText,
|
||||||
englishTitle: titleText,
|
englishTitle: titleText,
|
||||||
year: seasonData.air_date ? new Date(seasonData.air_date).getFullYear().toString() : 'unknown',
|
year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown',
|
||||||
dataSource: this.apiName,
|
dataSource: this.apiName,
|
||||||
id: `${tvId}/season/${seasonNumber}`,
|
id: `${tvId}/season/${seasonNumber}`,
|
||||||
seasonTitle: seasonData.name ?? titleText,
|
seasonTitle: season.name ?? titleText,
|
||||||
seasonNumber: seasonNumber,
|
seasonNumber: seasonNumber,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
53
src/main.ts
53
src/main.ts
|
|
@ -218,14 +218,67 @@ export default class MediaDbPlugin extends Plugin {
|
||||||
const proceed: boolean = false;
|
const proceed: boolean = false;
|
||||||
|
|
||||||
while (!proceed) {
|
while (!proceed) {
|
||||||
|
if (types.length === 1 && types[0] === 'season') {
|
||||||
|
selectResults =
|
||||||
|
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
||||||
|
return selectModalData.selected;
|
||||||
|
})) ?? [];
|
||||||
|
} else {
|
||||||
selectResults =
|
selectResults =
|
||||||
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
(await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => {
|
||||||
return await this.queryDetails(selectModalData.selected);
|
return await this.queryDetails(selectModalData.selected);
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
}
|
||||||
if (!selectResults || selectResults.length < 1) {
|
if (!selectResults || selectResults.length < 1) {
|
||||||
return;
|
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 => {
|
const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => {
|
||||||
return previewModalData.confirmed;
|
return previewModalData.confirmed;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
47
src/modals/MediaDbSeasonSelectModal.ts
Normal file
47
src/modals/MediaDbSeasonSelectModal.ts
Normal file
|
|
@ -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<SeasonSelectModalElement> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -75,6 +75,6 @@ export class SeasonModel extends MediaTypeModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary(): string {
|
getSummary(): string {
|
||||||
return 'Season ' + this.seasonNumber + '(' + this.year + ')';
|
return this.seasonNumber + ' seasons';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/utils/SeasonModalHelper.ts
Normal file
16
src/utils/SeasonModalHelper.ts
Normal file
|
|
@ -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<SeasonSelectModalElement[] | undefined> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const modal = new MediaDbSeasonSelectModal(plugin, seasons, true);
|
||||||
|
modal.setSubmitCallback(selected => {
|
||||||
|
resolve(selected);
|
||||||
|
});
|
||||||
|
modal.setCloseCallback(() => {
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
modal.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue