remove solid; rework season search

This commit is contained in:
Moritz Jung 2026-06-03 19:06:03 +02:00
parent ee985419e0
commit 21ad8c59d0
17 changed files with 407 additions and 357 deletions

View file

@ -1,9 +1,18 @@
import type MediaDbPlugin from 'packages/obsidian/src/main';
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 type { MediaType } from 'packages/obsidian/src/utils/MediaType';
import type { Result } from 'packages/obsidian/src/utils/result';
export interface SeasonListAPIModel extends APIModel {
getSeasonsForSeries(seriesId: string): Promise<Result<SeasonModel[], MDBError>>;
}
export function isSeasonListAPIModel(api: APIModel | undefined): api is SeasonListAPIModel {
return typeof api?.getSeasonsForSeries === 'function';
}
export abstract class APIModel {
apiName!: string;
apiUrl!: string;
@ -22,6 +31,12 @@ export abstract class APIModel {
abstract getDisabledMediaTypes(): MediaType[];
getSeasonsForSeries?(seriesId: string): Promise<Result<SeasonModel[], MDBError>>;
getSeasonApiNameForSeries(_series: MediaTypeModel): string | undefined {
return undefined;
}
hasType(type: MediaType): boolean {
const disabledMediaTypes = this.getDisabledMediaTypes();
return this.types.includes(type) && !disabledMediaTypes.includes(type);

View file

@ -3,6 +3,7 @@ import { APIModel } from 'packages/obsidian/src/api/APIModel';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { SeasonModel } from 'packages/obsidian/src/models/SeasonModel';
import { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel';
import { Logger } from 'packages/obsidian/src/utils/Logger';
import type { MDBError } from 'packages/obsidian/src/utils/MDBError';
import { MDBErrorKind, toMdbError } from 'packages/obsidian/src/utils/MDBError';
@ -153,14 +154,13 @@ export class TMDBSeasonAPI extends APIModel {
}
}
return new SeasonModel({
return new SeasonSearchResultModel({
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,
seasonCount: totalSeasons,
});
}),
);

View file

@ -252,4 +252,8 @@ export class TMDBSeriesAPI extends APIModel {
getDisabledMediaTypes(): MediaType[] {
return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes;
}
getSeasonApiNameForSeries(_series: MediaTypeModel): string {
return 'TMDBSeasonAPI';
}
}

View file

@ -0,0 +1,30 @@
import { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import type { ModelToData } from 'packages/obsidian/src/utils/Utils';
import { migrateObject } from 'packages/obsidian/src/utils/Utils';
export type SeasonSearchResultData = ModelToData<SeasonSearchResultModel>;
export class SeasonSearchResultModel extends MediaTypeModel {
seasonCount: number;
constructor(obj: SeasonSearchResultData) {
super();
this.seasonCount = 0;
migrateObject(this, obj, this);
this.type = this.getMediaType();
}
getTags(): string[] {
return [];
}
getMediaType(): MediaType {
return MediaType.Season;
}
getSummary(): string {
return `${this.seasonCount} ${this.seasonCount === 1 ? 'season' : 'seasons'}`;
}
}

View file

@ -1,24 +0,0 @@
import { onMount, Show } from 'solid-js';
import { setIcon } from 'obsidian';
interface IconProps {
iconName?: string;
}
export default function Icon(props: IconProps) {
let iconEl: HTMLDivElement | undefined;
onMount(() => {
if (iconEl) {
setIcon(iconEl, props.iconName || '');
}
});
return (
<Show when={(props.iconName || '').length > 0}>
<div class="icon-wrapper">
<div ref={iconEl} class="icon"></div>
</div>
</Show>
);
}

View file

@ -0,0 +1,159 @@
import { Component, setIcon } from 'obsidian';
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions } from 'packages/obsidian/src/settings/PropertyMapping';
import { capitalizeFirstLetter } from 'packages/obsidian/src/utils/Utils';
interface PropertyMappingModelComponentProps {
model: PropertyMappingModelData;
save: (model: PropertyMappingModelData) => void;
}
export default class PropertyMappingModelComponent extends Component {
private readonly containerEl: HTMLElement;
private readonly save: (model: PropertyMappingModelData) => void;
private modelData: PropertyMappingModelData;
private unsavedChanges = false;
constructor(containerEl: HTMLElement, props: PropertyMappingModelComponentProps) {
super();
this.containerEl = containerEl;
this.modelData = props.model;
this.save = props.save;
}
override onload(): void {
this.render();
}
override onunload(): void {
this.containerEl.empty();
}
private get validationResult(): { res: boolean; err?: Error } {
const model = PropertyMappingModel.fromJSON(this.modelData);
return model.validate();
}
private handleSave(): void {
const model = PropertyMappingModel.fromJSON(this.modelData);
if (!model.validate().res) {
return;
}
this.save(model.toJSON());
this.unsavedChanges = false;
this.render();
}
private onModelUpdate(): void {
this.unsavedChanges = true;
this.render();
}
private render(): void {
this.containerEl.empty();
const validationResult = this.validationResult;
const rootEl = this.containerEl.createDiv('media-db-plugin-property-mappings-model-container');
const headerEl = rootEl.createDiv('media-db-plugin-property-mappings-model-header');
headerEl.createDiv({ cls: 'setting-item-name', text: capitalizeFirstLetter(this.modelData.type) });
const actionsEl = headerEl.createDiv('media-db-plugin-property-mappings-model-actions');
if (this.unsavedChanges) {
actionsEl.createDiv({ cls: 'media-db-plugin-property-mapping-unsaved-changes', text: 'Unsaved changes' });
}
const saveButtonEl = actionsEl.createEl('button', {
cls: `media-db-plugin-property-mappings-save-button ${validationResult.res ? 'mod-cta' : 'mod-muted'}`,
text: 'Save',
});
saveButtonEl.addEventListener('click', () => this.handleSave());
if (!validationResult.res) {
rootEl.createDiv({
cls: 'media-db-plugin-property-mapping-validation',
text: validationResult.err?.message ?? '',
});
}
const tableContainerEl = rootEl.createDiv('media-db-plugin-property-mappings-table-container');
const tableEl = tableContainerEl.createEl('table', 'media-db-plugin-property-mappings-table');
const tableHeadEl = tableEl.createEl('thead');
const headerRowEl = tableHeadEl.createEl('tr');
headerRowEl.createEl('th', { cls: 'col-property', text: 'Property' });
headerRowEl.createEl('th', { cls: 'col-mapping', text: 'Mapping' });
headerRowEl.createEl('th', { cls: 'col-new-name', text: 'New name' });
headerRowEl.createEl('th', { cls: 'col-wikilink', text: 'Wikilink' });
const tableBodyEl = tableEl.createEl('tbody');
for (const [index, property] of this.modelData.properties.entries()) {
const rowEl = tableBodyEl.createEl('tr');
const propertyCellEl = rowEl.createEl('td', 'col-property');
propertyCellEl.createEl('code', { text: property.property });
if (property.locked) {
const lockedCellEl = rowEl.createEl('td', 'col-locked');
lockedCellEl.colSpan = 3;
lockedCellEl.createDiv({
cls: 'media-db-plugin-property-binding-text',
text: 'property cannot be remapped',
});
continue;
}
const mappingCellEl = rowEl.createEl('td', 'col-mapping');
const selectEl = mappingCellEl.createEl('select', 'dropdown');
for (const remappingOption of propertyMappingOptions) {
selectEl.createEl('option', {
attr: { value: remappingOption },
text: remappingOption,
});
}
selectEl.value = propertyMappingOptions.includes(property.mapping) ? property.mapping : PropertyMappingOption.Default;
selectEl.addEventListener('change', event => {
const targetEl = event.currentTarget as HTMLSelectElement;
this.modelData.properties[index].mapping = targetEl.value as PropertyMappingOption;
this.modelData.properties[index].newProperty = '';
this.onModelUpdate();
});
const newNameCellEl = rowEl.createEl('td', 'col-new-name');
if (property.mapping === PropertyMappingOption.Map) {
const mappingToEl = newNameCellEl.createDiv('media-db-plugin-property-mapping-to');
const iconWrapperEl = mappingToEl.createDiv('icon-wrapper');
const iconEl = iconWrapperEl.createDiv('icon');
setIcon(iconEl, 'arrow-right');
const inputEl = mappingToEl.createEl('input', {
cls: 'media-db-plugin-property-mapping-input',
type: 'text',
});
inputEl.spellcheck = false;
inputEl.value = property.newProperty;
inputEl.addEventListener('input', event => {
const targetEl = event.currentTarget as HTMLInputElement;
this.modelData.properties[index].newProperty = targetEl.value;
this.onModelUpdate();
});
} else {
newNameCellEl.createSpan({
cls: 'media-db-plugin-property-mapping-to-disabled',
text: 'N/A',
});
}
const wikilinkCellEl = rowEl.createEl('td', 'col-wikilink');
const wikilinkLabelEl = wikilinkCellEl.createEl('label', {
cls: 'media-db-plugin-property-mapping-wikilink-label',
attr: { title: 'Convert value to wikilink ([[value]])' },
});
const wikilinkInputEl = wikilinkLabelEl.createEl('input', { type: 'checkbox' });
wikilinkInputEl.checked = property.wikilink ?? false;
wikilinkInputEl.addEventListener('change', event => {
const targetEl = event.currentTarget as HTMLInputElement;
this.modelData.properties[index].wikilink = targetEl.checked;
this.onModelUpdate();
});
}
}
}

View file

@ -1,138 +0,0 @@
import { createSignal, createMemo, For, Show } from 'solid-js';
import { createStore } from 'solid-js/store';
import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping';
import { capitalizeFirstLetter } from '../utils/Utils';
import Icon from './Icon';
interface PropertyMappingModelComponentProps {
model: PropertyMappingModelData;
save: (model: PropertyMappingModelData) => void;
}
export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) {
const [unsavedChanges, setUnsavedChanges] = createSignal(false);
// Create a store from the model's plain data
const [modelData, setModelData] = createStore(props.model);
// Derive the validation result reactively
const validationResult = createMemo(() => {
const model = PropertyMappingModel.fromJSON(modelData);
return model.validate();
});
const onModelUpdate = () => {
setUnsavedChanges(true);
};
const handleSave = () => {
const model = PropertyMappingModel.fromJSON(modelData);
if (model.validate().res) {
props.save(model);
setUnsavedChanges(false);
}
};
return (
<div class="media-db-plugin-property-mappings-model-container">
<div class="media-db-plugin-property-mappings-model-header">
<div class="setting-item-name">{capitalizeFirstLetter(modelData.type)}</div>
<div class="media-db-plugin-property-mappings-model-actions">
<Show when={unsavedChanges()}>
<div class="media-db-plugin-property-mapping-unsaved-changes">Unsaved changes</div>
</Show>
<button class={`media-db-plugin-property-mappings-save-button ${validationResult().res ? 'mod-cta' : 'mod-muted'}`} onClick={handleSave}>
Save
</button>
</div>
</div>
<Show when={!validationResult().res}>
<div class="media-db-plugin-property-mapping-validation">{validationResult().err?.message}</div>
</Show>
<div class="media-db-plugin-property-mappings-table-container">
<table class="media-db-plugin-property-mappings-table">
<thead>
<tr>
<th class="col-property">Property</th>
<th class="col-mapping">Mapping</th>
<th class="col-new-name">New name</th>
<th class="col-wikilink">Wikilink</th>
</tr>
</thead>
<tbody>
<For each={modelData.properties}>
{(property, index) => (
<tr>
<td class="col-property">
<code>{property.property}</code>
</td>
<Show
when={!property.locked}
fallback={
<td class="col-locked" colspan={3}>
<div class="media-db-plugin-property-binding-text">property cannot be remapped</div>
</td>
}
>
<td class="col-mapping">
<select
class="dropdown"
value={property.mapping}
onChange={e => {
setModelData('properties', index(), 'mapping', e.currentTarget.value as PropertyMappingOption);
setModelData('properties', index(), 'newProperty', '');
onModelUpdate();
}}
>
<For each={propertyMappingOptions}>{remappingOption => <option value={remappingOption}>{remappingOption}</option>}</For>
</select>
</td>
<td class="col-new-name">
<Show
when={property.mapping === PropertyMappingOption.Map}
fallback={<span class="media-db-plugin-property-mapping-to-disabled">N/A</span>}
>
<div class="media-db-plugin-property-mapping-to">
<Icon iconName="arrow-right" />
<input
class="media-db-plugin-property-mapping-input"
type="text"
spellcheck={false}
value={property.newProperty}
onInput={e => {
setModelData('properties', index(), 'newProperty', e.currentTarget.value);
onModelUpdate();
}}
/>
</div>
</Show>
</td>
<td class="col-wikilink">
<label class="media-db-plugin-property-mapping-wikilink-label" title="Convert value to wikilink ([[value]])">
<input
type="checkbox"
checked={property.wikilink}
onChange={e => {
setModelData('properties', index(), 'wikilink', e.currentTarget.checked);
onModelUpdate();
}}
/>
</label>
</td>
</Show>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}

View file

@ -0,0 +1,40 @@
import { Component } from 'obsidian';
import type { PropertyMappingModelData } from 'packages/obsidian/src/settings/PropertyMapping';
import PropertyMappingModelComponent from 'packages/obsidian/src/settings/PropertyMappingModelComponent';
interface PropertyMappingModelsComponentProps {
models?: PropertyMappingModelData[];
save: (model: PropertyMappingModelData) => void;
}
export default class PropertyMappingModelsComponent extends Component {
private readonly containerEl: HTMLElement;
private readonly models: PropertyMappingModelData[];
private readonly save: (model: PropertyMappingModelData) => void;
constructor(containerEl: HTMLElement, props: PropertyMappingModelsComponentProps) {
super();
this.containerEl = containerEl;
this.models = props.models ?? [];
this.save = props.save;
}
override onload(): void {
this.containerEl.empty();
const rootEl = this.containerEl.createDiv('setting-item media-db-plugin-property-mappings-models-container');
for (const model of this.models) {
const modelContainerEl = rootEl.createDiv();
this.addChild(
new PropertyMappingModelComponent(modelContainerEl, {
model,
save: this.save,
}),
);
}
}
override onunload(): void {
this.containerEl.empty();
}
}

View file

@ -1,16 +0,0 @@
import { For } from 'solid-js';
import { type PropertyMappingModelData } from './PropertyMapping';
import PropertyMappingModelComponent from './PropertyMappingModelComponent';
interface PropertyMappingModelsComponentProps {
models?: PropertyMappingModelData[];
save: (model: PropertyMappingModelData) => void;
}
export default function PropertyMappingModelsComponent(props: PropertyMappingModelsComponentProps) {
return (
<div class="setting-item" style={{ display: 'flex', gap: '10px', 'flex-direction': 'column', 'align-items': 'stretch' }}>
<For each={props.models || []}>{model => <PropertyMappingModelComponent model={model} save={props.save} />}</For>
</div>
);
}

View file

@ -10,7 +10,6 @@ import { FolderSuggest } from 'packages/obsidian/src/settings/suggesters/FolderS
import { MediaType } from 'packages/obsidian/src/utils/MediaType';
import { MEDIA_TYPES } from 'packages/obsidian/src/utils/MediaTypeManager';
import { unCamelCase } from 'packages/obsidian/src/utils/Utils';
import { render } from 'solid-js/web';
function createDateFormatDescription(preview: string): DocumentFragment {
return createFragment(frag => {
@ -425,14 +424,25 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings
// MARK: Settings Tab
export class MediaDbSettingTab extends PluginSettingTab {
plugin: MediaDbPlugin;
private propertyMappingModelsComponent?: PropertyMappingModelsComponent;
constructor(app: App, plugin: MediaDbPlugin) {
super(app, plugin);
this.plugin = plugin;
}
override hide(): void {
this.propertyMappingModelsComponent?.unload();
this.propertyMappingModelsComponent = undefined;
super.hide();
}
display(): void {
const { containerEl } = this;
if (this.propertyMappingModelsComponent) {
this.propertyMappingModelsComponent.unload();
this.propertyMappingModelsComponent = undefined;
}
containerEl.empty();
const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt));
@ -837,24 +847,21 @@ export class MediaDbSettingTab extends PluginSettingTab {
mappingGroup.setHeading('Property mappings');
mappingGroup.addSetting(setting => {
setting.setName('Property mappings explanation').setDesc(createPropertyMappingsDescription());
const propertyMappingsEl = setting.descEl.createDiv();
this.propertyMappingModelsComponent = new PropertyMappingModelsComponent(propertyMappingsEl, {
models: structuredClone(this.plugin.settings.propertyMappingModels),
save: (model: PropertyMappingModelData): void => {
// Update the matching model in settings (stored as plain data)
const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type);
if (index !== -1) {
this.plugin.settings.propertyMappingModels[index] = model;
}
render(
() =>
PropertyMappingModelsComponent({
models: structuredClone(this.plugin.settings.propertyMappingModels),
save: (model: PropertyMappingModelData): void => {
// Update the matching model in settings (stored as plain data)
const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type);
if (index !== -1) {
this.plugin.settings.propertyMappingModels[index] = model;
}
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
void this.plugin.saveSettings();
},
}),
setting.descEl,
);
new Notice(`MDB: Property mappings for ${model.type} saved successfully.`);
void this.plugin.saveSettings();
},
});
this.propertyMappingModelsComponent.load();
});
}
}

View file

@ -120,6 +120,13 @@ small.media-db-plugin-list-text {
}
/* Property Mapping Component Styles */
.media-db-plugin-property-mappings-models-container {
display: flex;
gap: 10px;
flex-direction: column;
align-items: stretch;
}
.media-db-plugin-property-mappings-model-container {
margin-bottom: var(--size-4-8);
}

View file

@ -1,7 +1,7 @@
import type { TFolder } from 'obsidian';
import { TFile } from 'obsidian';
import type MediaDbPlugin from 'packages/obsidian/src/main';
import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
import { MediaDbBulkImportModal } from 'packages/obsidian/src/modals/MediaDbBulkImportModal';
import type { MediaTypeModel } from 'packages/obsidian/src/models/MediaTypeModel';
import { OutcomeStatus } from 'packages/obsidian/src/utils/result';
import { dateTimeToString, markdownTable } from 'packages/obsidian/src/utils/Utils';
@ -26,7 +26,7 @@ export class BulkImportHelper {
async import(folder: TFolder): Promise<void> {
const erroredFiles: BulkImportError[] = [];
let canceled: boolean = false;
let canceled = false;
const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{
selectedAPI: string;
@ -87,7 +87,11 @@ export class BulkImportHelper {
}
if (modelResult.value) {
await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined);
const createResult = await this.plugin.fileHelper.createMediaDbNotes([modelResult.value], appendContent ? file : undefined);
if (!createResult.ok) {
return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message };
}
return undefined;
}
@ -127,8 +131,25 @@ export class BulkImportHelper {
return { filePath: file.path, error: `no search results selected` };
}
const seasonSelectionResult = await this.plugin.entryHelper.handleSeasonSearchSelections(selectModalResult.data.selected, appendContent ? file : undefined);
if (seasonSelectionResult.handled) {
if (!seasonSelectionResult.created) {
return { filePath: file.path, error: 'season selection canceled or no season note was created' };
}
return undefined;
}
const detailedResults = await this.plugin.entryHelper.queryDetails(selectModalResult.data.selected);
await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
if (detailedResults.length === 0) {
return { filePath: file.path, error: 'failed to load details for selected search results' };
}
const createResult = await this.plugin.fileHelper.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
if (!createResult.ok) {
return { filePath: file.path, error: createResult.error.userMessage ?? createResult.error.message };
}
return undefined;
}

View file

@ -1,9 +1,12 @@
import type { TFile } from 'obsidian';
import { MarkdownView, Notice } from 'obsidian';
import type { TMDBSeasonAPI } from 'packages/obsidian/src/api/apis/TMDBSeasonAPI';
import type { SeasonListAPIModel } from 'packages/obsidian/src/api/APIModel';
import { isSeasonListAPIModel } from 'packages/obsidian/src/api/APIModel';
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 { SeasonSearchResultModel } from 'packages/obsidian/src/models/SeasonSearchResultModel';
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';
@ -112,6 +115,7 @@ export class MediaDbEntryHelper {
types.length === 1 && types[0] === MediaType.Season
? await this.plugin.modalHelper.promptSelectModal({
elements: filteredSearchResults,
multiSelect: false,
description: 'Select one search result to proceed.',
submitButtonText: 'Ok',
})
@ -121,7 +125,11 @@ export class MediaDbEntryHelper {
return;
}
const selectResults = types.length === 1 && types[0] === MediaType.Season ? selectResultsData.selected : await this.queryDetails(selectResultsData.selected);
if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) {
return;
}
const selectResults = await this.queryDetails(selectResultsData.selected);
if (selectResults.length === 0) {
return;
@ -167,6 +175,10 @@ export class MediaDbEntryHelper {
return;
}
if ((await this.handleSeasonSearchSelections(selectResultsData.selected)).handled) {
return;
}
const selectResults = await this.queryDetails(selectResultsData.selected);
if (selectResults.length < 1) {
return;
@ -239,31 +251,62 @@ export class MediaDbEntryHelper {
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 };
private async handleSeasonWorkflow(types: MediaType[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> {
if (!types.includes(MediaType.Series) || !types.includes(MediaType.Season)) {
return { handled: false };
}
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 };
}
const seriesResults = selectResults.filter(result => result.getMediaType() === MediaType.Series);
if (seriesResults.length !== 1) {
return { handled: false };
}
const seriesResult = seriesResults[0];
const sourceApi = this.plugin.apiManager.getApiByName(seriesResult.dataSource);
const seasonApiName = sourceApi?.getSeasonApiNameForSeries(seriesResult);
if (seasonApiName) {
const created = await this.showSeasonSelectAndCreate(seriesResult.id, seriesResult.title, undefined, seasonApiName);
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.');
private isSeasonSearchResult(model: MediaTypeModel): model is SeasonSearchResultModel {
return model instanceof SeasonSearchResultModel || (model.getMediaType() === MediaType.Season && typeof (model as { seasonCount?: unknown }).seasonCount === 'number');
}
async handleSeasonSearchSelections(selectedResults: MediaTypeModel[], attachFile?: TFile): Promise<{ handled: boolean; created: boolean }> {
const seasonSearchResults = selectedResults.filter(result => this.isSeasonSearchResult(result));
if (seasonSearchResults.length === 0) {
return { handled: false, created: false };
}
if (selectedResults.length !== 1) {
new Notice('Select exactly one season search result before choosing seasons.');
return { handled: true, created: false };
}
const selectedResult = seasonSearchResults[0];
return {
handled: true,
created: await this.showSeasonSelectAndCreate(selectedResult.id, selectedResult.englishTitle || selectedResult.title, attachFile, selectedResult.dataSource),
};
}
private getSeasonListApi(apiName: string): SeasonListAPIModel | undefined {
const api = this.plugin.apiManager.getApiByName(apiName);
return isSeasonListAPIModel(api) ? api : undefined;
}
private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string, attachFile: TFile | undefined, seasonApiName: string): Promise<boolean> {
const seasonAPI = this.getSeasonListApi(seasonApiName);
if (!seasonAPI) {
new Notice(`${seasonApiName} does not support season selection.`);
return false;
}
const allSeasonsResult = await tmdbSeasonAPI.getSeasonsForSeries(seriesId);
const allSeasonsResult = await seasonAPI.getSeasonsForSeries(seriesId);
if (!allSeasonsResult.ok) {
this.reportMdbError(allSeasonsResult.error);
new Notice(`Error loading seasons: ${allSeasonsResult.error.userMessage}`);
@ -281,8 +324,13 @@ export class MediaDbEntryHelper {
return false;
}
await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI);
new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`);
const createdCount = await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, seasonAPI, attachFile);
if (createdCount === 0) {
new Notice('No season entries were created.');
return false;
}
new Notice(`Successfully created ${createdCount} season ${createdCount === 1 ? 'entry' : 'entries'}.`);
return true;
}
@ -300,21 +348,36 @@ export class MediaDbEntryHelper {
});
}
private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise<void> {
await Promise.all(
private async createNotesForSelectedSeasons(
selectedSeasons: SeasonSelectModalElement[],
allSeasons: SeasonModel[],
seasonAPI: SeasonListAPIModel,
attachFile?: TFile,
): Promise<number> {
const results = 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);
const fullMetadataResult = await seasonAPI.getById(seasonModel.id);
if (!fullMetadataResult.ok) {
this.reportMdbError(fullMetadataResult.error);
new Notice(`Failed to load season ${selectedSeason.season_number}: ${fullMetadataResult.error.userMessage}`);
return;
return false;
}
await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value]);
const createResult = await this.plugin.fileHelper.createMediaDbNotes([fullMetadataResult.value], attachFile);
if (!createResult.ok) {
this.reportMdbError(createResult.error);
return false;
}
return true;
}
return false;
}),
);
return results.filter(created => created).length;
}
}