obsidian-media-db-sync/packages/obsidian/src/utils/MediaDbEntryHelper.ts
2026-05-21 20:14:11 +02:00

320 lines
11 KiB
TypeScript

import { MarkdownView, Notice } from 'obsidian';
import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { SeasonSelectModalElement } from 'packages/obsidian/src/modals/MediaDbSeasonSelectModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import type { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
import { MDBErrorKind } from 'packages/obsidian/src/utils/MDBError';
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import type { ModalLifecycle, ModalSession, SearchModalOptions } from 'packages/obsidian/src/utils/ModalHelper';
import type { Outcome } from 'packages/obsidian/src/utils/result';
import { OutcomeStatus } from 'packages/obsidian/src/utils/result';
export class MediaDbEntryHelper {
readonly plugin: MediaDbPlugin;
constructor(plugin: MediaDbPlugin) {
this.plugin = plugin;
}
private reportMdbError(error: MDBError): void {
this.plugin.errorReporter.report(error);
}
private getModalData<T>(modalResult: Outcome<T, MDBError>): T | undefined {
if (modalResult.status === OutcomeStatus.Ok) {
return modalResult.data;
}
if (modalResult.status === OutcomeStatus.Error) {
this.reportMdbError(modalResult.error);
}
return undefined;
}
private async runModalQuery<TData, TResult>(session: ModalSession<TData, ModalLifecycle>, query: () => Promise<TResult>): Promise<TResult | undefined> {
return this.getModalData(await this.plugin.modalHelper.runModalTask(session, query, { kind: MDBErrorKind.Api, message: 'API query failed' }));
}
async createLinkWithSearchModal(): Promise<void> {
const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({});
const advancedSearch = this.getModalData(advancedSearchSession.modalResult);
if (!advancedSearch) {
return;
}
const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis));
if (!apiSearchResults) {
return;
}
if (!apiSearchResults.ok) {
this.reportMdbError(apiSearchResults.error);
return;
}
if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) {
this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' });
return;
}
const selectResults = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items, multiSelect: false });
if (!selectResults || selectResults.selected.length < 1) {
return;
}
const detailedResults = await this.queryDetails(selectResults.selected);
if (detailedResults.length < 1) {
return;
}
const link = `[${detailedResults[0].title}](${detailedResults[0].url})`;
const view = this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
view.editor.replaceRange(link, view.editor.getCursor());
}
}
async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise<void> {
const searchSession = await this.plugin.modalHelper.createSearchModalSession(searchModalOptions ?? {});
const searchData = this.getModalData(searchSession.modalResult);
if (!searchData) {
return;
}
const types = searchData.types;
const apis = this.plugin.apiManager.apis.filter(api => api.hasTypeOverlap(types)).map(api => api.apiName);
const apiSearchResults = await this.runModalQuery(searchSession, () => this.plugin.apiManager.query(searchData.query, apis));
if (!apiSearchResults) {
return;
}
if (!apiSearchResults.ok) {
this.reportMdbError(apiSearchResults.error);
return;
}
if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) {
this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' });
return;
}
const filteredSearchResults = apiSearchResults.value.items.filter(result => types.includes(result.getMediaType()));
if (filteredSearchResults.length === 0) {
this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found for the selected types.', userMessage: 'No results found for the selected types.' });
return;
}
const selectResultsData =
types.length === 1 && types[0] === MediaType.Season
? await this.plugin.modalHelper.promptSelectModal({
elements: filteredSearchResults,
description: 'Select one search result to proceed.',
submitButtonText: 'Ok',
})
: await this.plugin.modalHelper.promptSelectModal({ elements: filteredSearchResults });
if (!selectResultsData) {
return;
}
const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected);
if (selectResults.length === 0) {
return;
}
const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults);
if (seasonHandlingResult.handled) {
return;
}
const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults });
if (!confirmed?.confirmed) {
return;
}
await this.plugin.fileHelper.createMediaDbNotes(selectResults);
}
async createEntryWithAdvancedSearchModal(): Promise<void> {
const advancedSearchSession = await this.plugin.modalHelper.createAdvancedSearchModalSession({});
const advancedSearch = this.getModalData(advancedSearchSession.modalResult);
if (!advancedSearch) {
return;
}
const apiSearchResults = await this.runModalQuery(advancedSearchSession, () => this.plugin.apiManager.query(advancedSearch.query, advancedSearch.apis));
if (!apiSearchResults) {
return;
}
if (!apiSearchResults.ok) {
this.reportMdbError(apiSearchResults.error);
return;
}
if (!apiSearchResults.value.items || apiSearchResults.value.items.length === 0) {
this.reportMdbError({ kind: MDBErrorKind.Validation, message: 'No results found.', userMessage: 'No results found.' });
return;
}
const selectResultsData = await this.plugin.modalHelper.promptSelectModal({ elements: apiSearchResults.value.items });
if (!selectResultsData) {
return;
}
const selectResults = await this.queryDetails(selectResultsData.selected);
if (selectResults.length < 1) {
return;
}
const confirmed = await this.plugin.modalHelper.promptPreviewModal({ elements: selectResults });
if (!confirmed?.confirmed) {
return;
}
await this.plugin.fileHelper.createMediaDbNotes(selectResults);
}
async createEntryWithIdSearchModal(): Promise<void> {
let idSearchResult: MediaTypeModel | undefined = undefined;
let proceed = false;
while (!proceed) {
const idSearchSession = await this.plugin.modalHelper.createIdSearchModalSession({});
const idSearchData = this.getModalData(idSearchSession.modalResult);
if (!idSearchData) {
return;
}
const queriedIdResult = await this.runModalQuery(idSearchSession, () => this.plugin.apiManager.queryDetailedInfoById(idSearchData.query, idSearchData.api));
if (!queriedIdResult) {
return;
}
if (!queriedIdResult.ok) {
this.reportMdbError(queriedIdResult.error);
return;
}
idSearchResult = queriedIdResult.value;
if (!idSearchResult) {
return;
}
const previewData = await this.plugin.modalHelper.promptPreviewModal({ elements: [idSearchResult] });
if (!previewData) {
return;
}
proceed = previewData.confirmed;
}
if (!idSearchResult) {
return;
}
const createNoteResult = await this.plugin.fileHelper.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true });
if (!createNoteResult.ok) {
this.reportMdbError(createNoteResult.error);
}
}
async queryDetails(models: MediaTypeModel[]): Promise<MediaTypeModel[]> {
const results = await Promise.all(models.map(model => this.plugin.apiManager.queryDetailedInfo(model)));
const detailModels: MediaTypeModel[] = [];
for (const result of results) {
if (result.ok && result.value) {
detailModels.push(result.value);
} else if (!result.ok) {
this.reportMdbError(result.error);
}
}
return detailModels;
}
private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> {
if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') {
const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title);
return { handled: true, seasonsCreated: created };
}
if (types.includes('series') && selectResults.some(result => result.dataSource === 'TMDBSeriesAPI')) {
const seriesResults = selectResults.filter(result => result.dataSource === 'TMDBSeriesAPI');
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 };
}
private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise<boolean> {
const tmdbSeasonAPI = this.plugin.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI | undefined;
if (!tmdbSeasonAPI) {
new Notice('TMDBSeasonAPI not available.');
return false;
}
const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId);
if (!allSeasonsResult.ok) {
this.reportMdbError(allSeasonsResult.error);
new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`);
return false;
}
const allSeasons = allSeasonsResult.value;
if (allSeasons.length === 0) {
new Notice('No seasons found for this series.');
return false;
}
const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle);
if (!selectedSeasons || selectedSeasons.length === 0) {
return false;
}
await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI);
new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`);
return true;
}
private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise<SeasonSelectModalElement[] | undefined> {
return await this.plugin.modalHelper.promptSeasonSelectModal({
seasons: allSeasons.map(season => ({
season_number: season.seasonNumber,
name: season.seasonTitle || season.title,
episode_count: season.episodes || 0,
air_date: season.year,
poster_path: season.image,
})),
multiSelect: true,
seriesName: seriesTitle,
});
}
private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise<void> {
await Promise.all(
selectedSeasons.map(async selectedSeason => {
const seasonModel = allSeasons.find(season => season.seasonNumber === selectedSeason.season_number);
if (seasonModel) {
const fullMetadataResult = await tmdbSeasonAPI.getById(seasonModel.id);
if (!fullMetadataResult.ok) {
this.reportMdbError(fullMetadataResult.error);
new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`);
return;
}
await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]);
}
}),
);
}
}