Merge pull request #207 from Spydarlee/batch-import-by-id

Allow batch / folder import by id as well as title
This commit is contained in:
Moritz Jung 2025-09-03 22:08:47 +02:00 committed by GitHub
commit 9941f14008
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 133 additions and 70 deletions

View file

@ -26,6 +26,7 @@ import type { SearchModalOptions } from './utils/ModalHelper';
import { ModalHelper, ModalResultCode } from './utils/ModalHelper';
import type { CreateNoteOptions } from './utils/Utils';
import { dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils';
import { BulkImportLookupMethod } from 'src/utils/BulkImportLookupMethod';
export type Metadata = Record<string, unknown>;
@ -561,9 +562,14 @@ export default class MediaDbPlugin extends Plugin {
const erroredFiles: { filePath: string; error: string }[] = [];
let canceled: boolean = false;
const { selectedAPI, titleFieldName, appendContent } = await new Promise<{ selectedAPI: string; titleFieldName: string; appendContent: boolean }>(resolve => {
new MediaDbFolderImportModal(this.app, this, (selectedAPI: string, titleFieldName: string, appendContent: boolean) => {
resolve({ selectedAPI, titleFieldName, appendContent });
const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{
selectedAPI: string;
lookupMethod: string;
fieldName: string;
appendContent: boolean;
}>(resolve => {
new MediaDbFolderImportModal(this.app, this, (selectedAPI: string, lookupMethod: string, fieldName: string, appendContent: boolean) => {
resolve({ selectedAPI, lookupMethod, fieldName, appendContent });
}).open();
});
@ -576,55 +582,74 @@ export default class MediaDbPlugin extends Plugin {
}
const metadata = this.getMetadataFromFileCache(file);
const lookupValue = metadata[fieldName];
const title = metadata[titleFieldName];
if (!title || typeof title !== 'string') {
erroredFiles.push({ filePath: file.path, error: `metadata field '${titleFieldName}' not found, empty, or not a string` });
if (!lookupValue || typeof lookupValue !== 'string') {
erroredFiles.push({ filePath: file.path, error: `metadata field '${fieldName}' not found, empty, or not a string` });
continue;
}
} else if (lookupMethod === BulkImportLookupMethod.ID) {
try {
const model = await this.apiManager.queryDetailedInfoById(lookupValue, selectedAPI);
if (model) {
await this.createMediaDbNotes([model], appendContent ? file : undefined);
} else {
erroredFiles.push({ filePath: file.path, error: `Failed to query API with id: ${lookupValue}` });
}
} catch (e) {
erroredFiles.push({ filePath: file.path, error: `${e}` });
continue;
}
} else if (lookupMethod === BulkImportLookupMethod.TITLE) {
let results: MediaTypeModel[] = [];
try {
results = await this.apiManager.query(lookupValue, [selectedAPI]);
} catch (e) {
erroredFiles.push({ filePath: file.path, error: `${e}` });
continue;
}
if (!results || results.length === 0) {
erroredFiles.push({ filePath: file.path, error: `no search results` });
continue;
}
let results: MediaTypeModel[] = [];
try {
results = await this.apiManager.query(title, [selectedAPI]);
} catch (e) {
erroredFiles.push({ filePath: file.path, error: `${e}` });
continue;
}
if (!results || results.length === 0) {
erroredFiles.push({ filePath: file.path, error: `no search results` });
continue;
}
const { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({
elements: results,
skipButton: true,
modalTitle: `Results for '${lookupValue}'`,
});
const { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({ elements: results, skipButton: true, modalTitle: `Results for '${title}'` });
if (selectModalResult.code === ModalResultCode.ERROR) {
erroredFiles.push({ filePath: file.path, error: selectModalResult.error.message });
selectModal.close();
continue;
}
if (selectModalResult.code === ModalResultCode.CLOSE) {
erroredFiles.push({ filePath: file.path, error: 'user canceled' });
selectModal.close();
canceled = true;
continue;
}
if (selectModalResult.code === ModalResultCode.SKIP) {
erroredFiles.push({ filePath: file.path, error: 'user skipped' });
selectModal.close();
continue;
}
if (selectModalResult.data.selected.length === 0) {
erroredFiles.push({ filePath: file.path, error: `no search results selected` });
continue;
}
const detailedResults = await this.queryDetails(selectModalResult.data.selected);
await this.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
if (selectModalResult.code === ModalResultCode.ERROR) {
erroredFiles.push({ filePath: file.path, error: selectModalResult.error.message });
selectModal.close();
} else {
erroredFiles.push({ filePath: file.path, error: `invalid lookup type` });
continue;
}
if (selectModalResult.code === ModalResultCode.CLOSE) {
erroredFiles.push({ filePath: file.path, error: 'user canceled' });
selectModal.close();
canceled = true;
continue;
}
if (selectModalResult.code === ModalResultCode.SKIP) {
erroredFiles.push({ filePath: file.path, error: 'user skipped' });
selectModal.close();
continue;
}
if (selectModalResult.data.selected.length === 0) {
erroredFiles.push({ filePath: file.path, error: `no search results selected` });
continue;
}
const detailedResults = await this.queryDetails(selectModalResult.data.selected);
await this.createMediaDbNotes(detailedResults, appendContent ? file : undefined);
selectModal.close();
}
}

View file

@ -1,26 +1,30 @@
import type { App, ButtonComponent } from 'obsidian';
import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian';
import type MediaDbPlugin from '../main';
import type { APIModel } from 'src/api/APIModel';
import { BulkImportLookupMethod } from 'src/utils/BulkImportLookupMethod';
export class MediaDbFolderImportModal extends Modal {
plugin: MediaDbPlugin;
onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void;
onSubmit: (selectedAPI: string, lookupMethod: string, fieldName: string, appendContent: boolean) => void;
selectedApi: string;
searchBtn?: ButtonComponent;
titleFieldName: string;
lookupMethod: string;
fieldName: string;
appendContent: boolean;
constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string, appendContent: boolean) => void) {
constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: string, fieldName: string, appendContent: boolean) => void) {
super(app);
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApi = plugin.apiManager.apis[0].apiName;
this.titleFieldName = '';
this.lookupMethod = BulkImportLookupMethod.TITLE;
this.fieldName = '';
this.appendContent = false;
}
submit(): void {
this.onSubmit(this.selectedApi, this.titleFieldName, this.appendContent);
this.onSubmit(this.selectedApi, this.lookupMethod, this.fieldName, this.appendContent);
this.close();
}
@ -29,21 +33,19 @@ export class MediaDbFolderImportModal extends Modal {
contentEl.createEl('h2', { text: 'Import folder as Media DB entries' });
const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
const apiSelectorTextWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
apiSelectorTextWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' });
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
apiSelectorComponent.onChange((value: string) => {
this.selectedApi = value;
});
for (const api of this.plugin.apiManager.apis) {
apiSelectorComponent.addOption(api.apiName, api.apiName);
}
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);
this.createDropdownEl(
contentEl,
'API to search',
(value: string) => {
this.selectedApi = value;
},
this.plugin.apiManager.apis.map((api: APIModel) => {
return { value: api.apiName, display: api.apiName };
}),
);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'Append note content to Media DB entry.' });
contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' });
const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
@ -60,19 +62,38 @@ export class MediaDbFolderImportModal extends Modal {
appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
contentEl.createEl('h3', { text: 'The name of the metadata field that should be used as the title to query.' });
contentEl.createEl('h3', { text: 'Media lookup method' });
contentEl.createEl('p', {
text: 'Choose whether to search the API by title (can return multiple results) or lookup directly using an ID (returns at most one result), and specify the name of the frontmatter property which contains the title or ID of the media.',
});
const placeholder = 'title';
const titleFieldNameComponent = new TextComponent(contentEl);
titleFieldNameComponent.inputEl.style.width = '100%';
titleFieldNameComponent.setPlaceholder(placeholder);
titleFieldNameComponent.onChange(value => (this.titleFieldName = value));
titleFieldNameComponent.inputEl.addEventListener('keydown', ke => {
this.createDropdownEl(
contentEl,
'Lookup media by',
(value: string) => {
this.lookupMethod = value;
},
[
{ value: BulkImportLookupMethod.TITLE, display: 'Title' },
{ value: BulkImportLookupMethod.ID, display: 'ID' },
],
);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' });
const fieldNameComponent = new TextComponent(fieldNameWrapperEl);
fieldNameComponent.setPlaceholder('title / id');
fieldNameComponent.onChange(value => (this.fieldName = value));
fieldNameComponent.inputEl.addEventListener('keydown', ke => {
if (ke.key === 'Enter') {
this.submit();
}
});
contentEl.appendChild(titleFieldNameComponent.inputEl);
contentEl.appendChild(fieldNameWrapperEl);
contentEl.createDiv({ cls: 'media-db-plugin-spacer' });
@ -93,6 +114,19 @@ export class MediaDbFolderImportModal extends Modal {
});
}
createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void {
const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' });
const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' });
labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' });
const dropDownComponent = new DropdownComponent(wrapperEl);
dropDownComponent.onChange(onChange);
for (const option of options) {
dropDownComponent.addOption(option.value, option.display);
}
wrapperEl.appendChild(dropDownComponent.selectEl);
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();

View file

@ -0,0 +1,4 @@
export enum BulkImportLookupMethod {
ID = 'id',
TITLE = 'title',
}