diff --git a/src/main.ts b/src/main.ts index b2aea68..f140aab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ -import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFile, TFolder } from 'obsidian'; +import type { TFile } from 'obsidian'; +import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian'; import { requestUrl, normalizePath } from 'obsidian'; import type { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; @@ -14,19 +15,18 @@ import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { SteamAPI } from './api/apis/SteamAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; -import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal'; import type { MediaTypeModel } from './models/MediaTypeModel'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings'; +import { BulkImportHelper } from './utils/BulkImportHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; import type { SearchModalOptions } from './utils/ModalHelper'; -import { ModalHelper, ModalResultCode } from './utils/ModalHelper'; +import { ModalHelper } 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'; +import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; export type Metadata = Record; @@ -42,6 +42,7 @@ export default class MediaDbPlugin extends Plugin { mediaTypeManager!: MediaTypeManager; modelPropertyMapper!: PropertyMapper; modalHelper!: ModalHelper; + bulkImportHelper!: BulkImportHelper; dateFormatter!: DateFormatter; frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; @@ -64,6 +65,7 @@ export default class MediaDbPlugin extends Plugin { this.mediaTypeManager = new MediaTypeManager(); this.modelPropertyMapper = new PropertyMapper(this); this.modalHelper = new ModalHelper(this); + this.bulkImportHelper = new BulkImportHelper(this); this.dateFormatter = new DateFormatter(); await this.loadSettings(); @@ -84,7 +86,7 @@ export default class MediaDbPlugin extends Plugin { menu.addItem(item => { item.setTitle('Import folder as Media DB entries') .setIcon('database') - .onClick(() => this.createEntriesFromFolder(file)); + .onClick(() => this.bulkImportHelper.import(file)); }); } }), @@ -558,116 +560,6 @@ export default class MediaDbPlugin extends Plugin { } } - async createEntriesFromFolder(folder: TFolder): Promise { - const erroredFiles: { filePath: string; error: string }[] = []; - let canceled: boolean = false; - - 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(); - }); - - for (const child of folder.children) { - if (child instanceof TFile) { - const file: TFile = child; - if (canceled) { - erroredFiles.push({ filePath: file.path, error: 'user canceled' }); - continue; - } - - const metadata = this.getMetadataFromFileCache(file); - const lookupValue = metadata[fieldName]; - - 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; - } - - const { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({ - elements: results, - skipButton: true, - modalTitle: `Results for '${lookupValue}'`, - }); - - 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); - - selectModal.close(); - } else { - erroredFiles.push({ filePath: file.path, error: `invalid lookup type` }); - continue; - } - } - } - - if (erroredFiles.length > 0) { - await this.createErroredFilesReport(erroredFiles); - } - } - - async createErroredFilesReport(erroredFiles: { filePath: string; error: string }[]): Promise { - const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`; - const filePath = `${title}.md`; - - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - - const fileContent = `# ${title}\n\n${markdownTable(table)}`; - await this.app.vault.create(filePath, fileContent); - } - async loadSettings(): Promise { // console.log(DEFAULT_SETTINGS); const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbBulkImportModal.ts similarity index 88% rename from src/modals/MediaDbFolderImportModal.ts rename to src/modals/MediaDbBulkImportModal.ts index ef9bcf8..1aee8ee 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbBulkImportModal.ts @@ -1,20 +1,20 @@ -import type { App, ButtonComponent } from 'obsidian'; +import type { 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'; +import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper'; +import type MediaDbPlugin from '../main'; -export class MediaDbFolderImportModal extends Modal { +export class MediaDbBulkImportModal extends Modal { plugin: MediaDbPlugin; - onSubmit: (selectedAPI: string, lookupMethod: string, fieldName: string, appendContent: boolean) => void; + onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void; selectedApi: string; searchBtn?: ButtonComponent; - lookupMethod: string; + lookupMethod: BulkImportLookupMethod; fieldName: string; appendContent: boolean; - constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: string, fieldName: string, appendContent: boolean) => void) { - super(app); + constructor(plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void) { + super(plugin.app); this.plugin = plugin; this.onSubmit = onSubmit; this.selectedApi = plugin.apiManager.apis[0].apiName; @@ -71,7 +71,7 @@ export class MediaDbFolderImportModal extends Modal { contentEl, 'Lookup media by', (value: string) => { - this.lookupMethod = value; + this.lookupMethod = value as BulkImportLookupMethod; }, [ { value: BulkImportLookupMethod.TITLE, display: 'Title' }, diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts new file mode 100644 index 0000000..dcbaabb --- /dev/null +++ b/src/utils/BulkImportHelper.ts @@ -0,0 +1,150 @@ +import type { TFolder } from 'obsidian'; +import { TFile } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; +import type { MediaTypeModel } from 'src/models/MediaTypeModel'; +import { ModalResultCode } from './ModalHelper'; +import { dateTimeToString, markdownTable } from './Utils'; + +export enum BulkImportLookupMethod { + ID = 'id', + TITLE = 'title', +} + +interface BulkImportError { + filePath: string; + error: string; + canceled?: boolean; +} + +export class BulkImportHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async import(folder: TFolder): Promise { + const erroredFiles: BulkImportError[] = []; + let canceled: boolean = false; + + const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ + selectedAPI: string; + lookupMethod: BulkImportLookupMethod; + fieldName: string; + appendContent: boolean; + }>(resolve => { + new MediaDbBulkImportModal(this.plugin, (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => { + resolve({ selectedAPI, lookupMethod, fieldName, appendContent }); + }).open(); + }); + + for (const child of folder.children) { + if (!(child instanceof TFile)) { + continue; + } + + const file: TFile = child; + if (canceled) { + erroredFiles.push({ filePath: file.path, error: 'user canceled' }); + continue; + } + + const metadata = this.plugin.getMetadataFromFileCache(file); + const lookupValue = metadata[fieldName]; + + 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) { + const error = await this.importById(file, lookupValue, selectedAPI, appendContent); + if (error) { + erroredFiles.push(error); + } + } else if (lookupMethod === BulkImportLookupMethod.TITLE) { + const error = await this.importByTitle(file, lookupValue, selectedAPI, appendContent); + if (error) { + if (error.canceled) { + canceled = true; + } + erroredFiles.push(error); + } + } else { + erroredFiles.push({ filePath: file.path, error: `invalid lookup type` }); + continue; + } + } + + if (erroredFiles.length > 0) { + await this.createErroredFilesReport(erroredFiles); + } + } + + private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { + try { + const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); + if (model) { + await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined); + return undefined; + } else { + return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; + } + } catch (e) { + return { filePath: file.path, error: `${e}` }; + } + } + + private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { + let results: MediaTypeModel[] = []; + try { + results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); + } catch (e) { + return { filePath: file.path, error: `${e}` }; + } + if (!results || results.length === 0) { + return { filePath: file.path, error: `no search results` }; + } + + const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({ + elements: results, + skipButton: true, + modalTitle: `Results for '${lookupValue}'`, + }); + + if (selectModalResult.code === ModalResultCode.ERROR) { + selectModal.close(); + return { filePath: file.path, error: selectModalResult.error.message }; + } + + if (selectModalResult.code === ModalResultCode.CLOSE) { + selectModal.close(); + return { filePath: file.path, error: 'user canceled', canceled: true }; + } + + if (selectModalResult.code === ModalResultCode.SKIP) { + selectModal.close(); + return { filePath: file.path, error: 'user skipped' }; + } + + if (selectModalResult.data.selected.length === 0) { + selectModal.close(); + return { filePath: file.path, error: `no search results selected` }; + } + + const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); + await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + + selectModal.close(); + return undefined; + } + + private async createErroredFilesReport(erroredFiles: BulkImportError[]): Promise { + const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + + const fileContent = `# ${title}\n\n${markdownTable(table)}`; + await this.plugin.app.vault.create(filePath, fileContent); + } +} diff --git a/src/utils/BulkImportLookupMethod.ts b/src/utils/BulkImportLookupMethod.ts deleted file mode 100644 index d785723..0000000 --- a/src/utils/BulkImportLookupMethod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum BulkImportLookupMethod { - ID = 'id', - TITLE = 'title', -} diff --git a/src/utils/IllegalFilenameCharactersList.ts b/src/utils/IllegalFilenameCharactersList.ts index d0ceba1..220ca9a 100644 --- a/src/utils/IllegalFilenameCharactersList.ts +++ b/src/utils/IllegalFilenameCharactersList.ts @@ -1,6 +1,6 @@ // Illegal characters in the form `[illegal_character, replacement][]` export const ILLEGAL_FILENAME_CHARACTERS = [ - ['\/', '-'], + ['/', '-'], ['\\', '-'], ['<', ''], ['>', ''], diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 29c78a2..6f60c3b 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -10,9 +10,9 @@ import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { SeriesModel } from '../models/SeriesModel'; import { WikiModel } from '../models/WikiModel'; import type { MediaDbPluginSettings } from '../settings/Settings'; +import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; import { MediaType } from './MediaType'; import { replaceTags } from './Utils'; -import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; export const MEDIA_TYPES: MediaType[] = [ MediaType.Movie, @@ -72,7 +72,7 @@ export class MediaTypeManager { getFileName(mediaTypeModel: MediaTypeModel): string { // Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur - let fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true); + const fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true); return this.cleanFileName(fileName); }