From 2c3fe620590196a54c4509286885e34f42d01a1c Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Sat, 2 Jul 2022 13:19:59 +0200 Subject: [PATCH 1/2] fix build errors --- src/api/apis/SteamAPI.ts | 4 ++-- src/api/apis/WikipediaAPI.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index ef8f21d..faeb7aa 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.ts @@ -79,13 +79,13 @@ export class SteamAPI extends APIModel { debugLog(await fetchData.json); - let result; + let result: any; for (const [key, value] of Object.entries(await fetchData.json)) { // console.log(typeof key, key) // console.log(typeof id, id) // after some testing I found out that id is somehow a number despite that it's defined as string... if (key === String(id)) { - result = value.data; + result = (value as any).data; } } if (!result) { diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts index b0796b2..835e225 100644 --- a/src/api/apis/WikipediaAPI.ts +++ b/src/api/apis/WikipediaAPI.ts @@ -58,7 +58,7 @@ export class WikipediaAPI extends APIModel { const data = await fetchData.json(); debugLog(data); - const result = Object.entries(data?.query?.pages)[0][1]; + const result: any = Object.entries(data?.query?.pages)[0][1]; const model = new WikiModel({ type: 'wiki', From 6a0d192aba72e5cf44934a664fd51d25108e44b8 Mon Sep 17 00:00:00 2001 From: mProjectsCode Date: Thu, 22 Sep 2022 13:19:23 +0200 Subject: [PATCH 2/2] Some main.ts refactoring --- src/api/APIManager.ts | 8 +- src/main.ts | 179 +++++++++++++++-------- src/modals/MediaDbAdvancedSearchModal.ts | 28 ++-- src/modals/MediaDbIdSearchModal.ts | 15 +- src/modals/MediaDbSearchResultModal.ts | 6 +- src/settings/ModelPropertyMapper.ts | 26 +++- src/utils/MediaTypeManager.ts | 6 + src/utils/Utils.ts | 2 +- 8 files changed, 172 insertions(+), 98 deletions(-) diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index cca4a41..d1727b7 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -9,13 +9,13 @@ export class APIManager { this.apis = []; } - async query(query: string, apisToQuery: any): Promise { + async query(query: string, apisToQuery: string[]): Promise { debugLog(`MDB | api manager queried with "${query}"`); let res: MediaTypeModel[] = []; for (const api of this.apis) { - if (Object.keys(apisToQuery).contains(api.apiName) && apisToQuery[api.apiName]) { + if (apisToQuery.contains(api.apiName)) { const apiRes = await api.searchByTitle(query); res = res.concat(apiRes); } @@ -28,9 +28,9 @@ export class APIManager { return await this.queryDetailedInfoById(item.id, item.dataSource); } - async queryDetailedInfoById(id: string, dataSource: string): Promise { + async queryDetailedInfoById(id: string, apiName: string): Promise { for (const api of this.apis) { - if (api.apiName === dataSource) { + if (api.apiName === apiName) { return api.getById(id); } } diff --git a/src/main.ts b/src/main.ts index 2196f27..aba9251 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,12 +23,31 @@ export default class MediaDbPlugin extends Plugin { mediaTypeManager: MediaTypeManager; modelPropertyMapper: ModelPropertyMapper; + frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; + async onload() { await this.loadSettings(); + // register the settings tab + this.addSettingTab(new MediaDbSettingTab(this.app, this)); + + + this.apiManager = new APIManager(); + // register APIs + this.apiManager.registerAPI(new OMDbAPI(this)); + this.apiManager.registerAPI(new MALAPI(this)); + this.apiManager.registerAPI(new WikipediaAPI(this)); + this.apiManager.registerAPI(new MusicBrainzAPI(this)); + this.apiManager.registerAPI(new SteamAPI(this)); + this.apiManager.registerAPI(new BoardGameGeekAPI(this)); + // this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data + + this.mediaTypeManager = new MediaTypeManager(this.settings); + this.modelPropertyMapper = new ModelPropertyMapper(this.settings); + // add icon to the left ribbon const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', (evt: MouseEvent) => - this.createMediaDbNotes(this.openMediaDbAdvancedSearchModal.bind(this)), + this.createEntryWithAdvancedSearchModal(), ); ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); @@ -46,13 +65,13 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'open-media-db-search-modal', name: 'Add new Media DB entry', - callback: () => this.createMediaDbNotes(this.openMediaDbAdvancedSearchModal.bind(this)), + callback: () => this.createEntryWithAdvancedSearchModal(), }); // register command to open id search modal this.addCommand({ id: 'open-media-db-id-search-modal', name: 'Add new Media DB entry by id', - callback: () => this.createMediaDbNotes(this.openMediaDbIdSearchModal.bind(this)), + callback: () => this.createEntryWithIdSearchModal(), }); // register command to update the open note this.addCommand({ @@ -68,42 +87,65 @@ export default class MediaDbPlugin extends Plugin { return true; }, }); - - // register the settings tab - this.addSettingTab(new MediaDbSettingTab(this.app, this)); - - - this.apiManager = new APIManager(); - // register APIs - this.apiManager.registerAPI(new OMDbAPI(this)); - this.apiManager.registerAPI(new MALAPI(this)); - this.apiManager.registerAPI(new WikipediaAPI(this)); - this.apiManager.registerAPI(new MusicBrainzAPI(this)); - this.apiManager.registerAPI(new SteamAPI(this)); - this.apiManager.registerAPI(new BoardGameGeekAPI(this)); - // this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data - - this.mediaTypeManager = new MediaTypeManager(this.settings); - this.modelPropertyMapper = new ModelPropertyMapper(this.settings); } - async createMediaDbNotes(modal: () => Promise, attachFile?: TFile): Promise { - let models: MediaTypeModel[] = []; + async createEntryWithSearchModal() { + + } + + async createEntryWithAdvancedSearchModal() { + let results: MediaTypeModel[] = []; try { - models = await modal(); + const {query, apis} = await this.openMediaDbAdvancedSearchModal(); + + new Notice('MediaDB Searching...'); + + const apiSearchResults = await this.apiManager.query(query, apis); + const selectResults = await this.openMediaDbSelectModal(apiSearchResults, false); + results = await this.queryDetails(selectResults); } catch (e) { console.warn(e); new Notice(e.toString()); } + debugLog(results); + await this.createMediaDbNotes(results); + } + + async createEntryWithIdSearchModal() { + let result: MediaTypeModel = undefined; + try { + const {query, api} = await this.openMediaDbIdSearchModal(); + + new Notice('MediaDB Searching...'); + + result = await this.apiManager.queryDetailedInfoById(query, api); + } catch (e) { + console.warn(e); + new Notice(e.toString()); + } + + debugLog(result); + await this.createMediaDbNoteFromModel(result); + } + + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { + for (const model of models) { + await this.createMediaDbNoteFromModel(model, attachFile); + } + } + + async queryDetails(models: MediaTypeModel[]): Promise { + let detailModels: MediaTypeModel[] = []; for (const model of models) { try { - await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model), attachFile); + detailModels.push(await this.apiManager.queryDetailedInfo(model)); } catch (e) { console.warn(e); new Notice(e.toString()); } } + return detailModels; } async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, attachFile?: TFile): Promise { @@ -187,23 +229,33 @@ export default class MediaDbPlugin extends Plugin { return metadata; } + /** + * Creates a note in the vault. + * + * @param fileName + * @param fileContent + * @param openFile + */ async createNote(fileName: string, fileContent: string, openFile: boolean = false) { fileName = replaceIllegalFileNameCharactersInString(fileName); const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`; + // find and possibly create the folder set in settings const folder = this.app.vault.getAbstractFileByPath(this.settings.folder); if (!folder) { await this.app.vault.createFolder(this.settings.folder.replace(/\/$/, '')); } + // find and delete file with the same name const file = this.app.vault.getAbstractFileByPath(filePath); if (file) { await this.app.vault.delete(file); } + // create the file const targetFile = await this.app.vault.create(filePath, fileContent); - // open file + // open newly crated file if (openFile) { const activeLeaf = this.app.workspace.getUnpinnedLeaf(); if (!activeLeaf) { @@ -214,6 +266,10 @@ export default class MediaDbPlugin extends Plugin { } } + /** + * Update the active note by querying the API again. + * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. + */ async updateActiveNote() { const activeFile: TFile = this.app.workspace.getActiveFile(); if (!activeFile) { @@ -249,18 +305,12 @@ export default class MediaDbPlugin extends Plugin { const erroredFiles: { filePath: string, error: string }[] = []; let canceled: boolean = false; - const {selectedAPI, titleFieldName, appendContent} = await new Promise((resolve, reject) => { - new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName, appendContent) => { + const {selectedAPI, titleFieldName, appendContent} = await new Promise<{selectedAPI: string, titleFieldName: string, appendContent: boolean}>((resolve, reject) => { + new MediaDbFolderImportModal(this.app, this, ((selectedAPI: string, titleFieldName: string, appendContent: boolean) => { resolve({selectedAPI, titleFieldName, appendContent}); })).open(); }); - const selectedAPIs = {}; - for (const api of this.apiManager.apis) { - // @ts-ignore - selectedAPIs[api.apiName] = api.apiName === selectedAPI; - } - for (const child of folder.children) { if (child instanceof TFile) { const file = child as TFile; @@ -279,7 +329,7 @@ export default class MediaDbPlugin extends Plugin { let results: MediaTypeModel[] = []; try { - results = await this.apiManager.query(title, selectedAPIs); + results = await this.apiManager.query(title, [selectedAPI]); } catch (e) { erroredFiles.push({filePath: file.path, error: e.toString()}); continue; @@ -292,7 +342,7 @@ export default class MediaDbPlugin extends Plugin { let selectedResults: MediaTypeModel[] = []; try { selectedResults = await new Promise((resolve, reject) => { - const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, true, (err, res) => { + const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, true, (res, err) => { if (err) { return reject(err); } @@ -325,47 +375,60 @@ export default class MediaDbPlugin extends Plugin { continue; } - await this.createMediaDbNotes(async () => selectedResults, appendContent ? file : null); + const detailedResults = await this.queryDetails(selectedResults); + await this.createMediaDbNotes(detailedResults, appendContent ? file : null); } } if (erroredFiles.length > 0) { - const title = `bulk import error report ${dateTimeToString(new Date())}`; - const filePath = `${this.settings.folder.replace(/\/$/, '')}/${title}.md`; - - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - // console.log(table) - let fileContent = `# ${title}\n\n${markdownTable(table)}`; - - const targetFile = await this.app.vault.create(filePath, fileContent); + await this.createErroredFilesReport(erroredFiles); } } - async openMediaDbAdvancedSearchModal(): Promise { - return new Promise(((resolve, reject) => { - new MediaDbAdvancedSearchModal(this.app, this, (err, results) => { + async createErroredFilesReport(erroredFiles: { filePath: string, error: string }[]): Promise { + const title = `bulk import error report ${dateTimeToString(new Date())}`; + const filePath = `${this.settings.folder.replace(/\/$/, '')}/${title}.md`; + + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + // console.log(table) + let fileContent = `# ${title}\n\n${markdownTable(table)}`; + + const targetFile = await this.app.vault.create(filePath, fileContent); + } + + async openMediaDbAdvancedSearchModal(): Promise<{ query: string, apis: string[] }> { + return await new Promise((resolve, reject) => { + new MediaDbAdvancedSearchModal(this.app, this, (res, err) => { if (err) { return reject(err); } - new MediaDbSearchResultModal(this.app, this, results, false, (err2, res) => { - if (err2) { - return reject(err2); - } - resolve(res); - }, () => resolve([])).open(); + resolve(res) }).open(); - })); + }); } - async openMediaDbIdSearchModal(): Promise { - return new Promise(((resolve, reject) => { - new MediaDbIdSearchModal(this.app, this, (err, res) => { + async openMediaDbIdSearchModal(): Promise<{ query: string, api: string }> { + return await new Promise((resolve, reject) => { + new MediaDbIdSearchModal(this.app, this, (res, err) => { + if (err) { + return reject(err); + } + resolve(res) + }).open(); + }); + } + + async openMediaDbSelectModal(resultsToDisplay: MediaTypeModel[], skipButton: boolean = false): Promise { + return await new Promise((resolve, reject) => { + new MediaDbSearchResultModal(this.app, this, resultsToDisplay, skipButton, (res, err) => { if (err) { return reject(err); } resolve(res); + }, () => { + resolve([]) }).open(); - })); + }); } async loadSettings() { diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/src/modals/MediaDbAdvancedSearchModal.ts index 5b28370..c463f4c 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/src/modals/MediaDbAdvancedSearchModal.ts @@ -8,16 +8,16 @@ export class MediaDbAdvancedSearchModal extends Modal { isBusy: boolean; plugin: MediaDbPlugin; searchBtn: ButtonComponent; - selectedApis: any; - onSubmit: (err: Error, result?: MediaTypeModel[]) => void; + selectedApis: {name: string, selected: boolean}[]; + onSubmit: (res: {query: string, apis: string[]}, err?: Error) => void; - constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (err: Error, result?: MediaTypeModel[]) => void) { + constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (res: {query: string, apis: string[]}, err?: Error) => void) { super(app); this.plugin = plugin; this.onSubmit = onSubmit; this.selectedApis = []; for (const api of this.plugin.apiManager.apis) { - this.selectedApis[api.apiName] = false; + this.selectedApis.push({name: api.apiName, selected: false}); } } @@ -36,14 +36,9 @@ export class MediaDbAdvancedSearchModal extends Modal { return; } - let selectedAPICount = 0; - for (const api in this.selectedApis) { - if (this.selectedApis[api]) { - selectedAPICount += 1; - } - } + const apis: string[] = this.selectedApis.filter(x => x.selected).map(x => x.name); - if (selectedAPICount === 0) { + if (apis.length === 0) { new Notice('MDB | No API selected'); return; } @@ -54,12 +49,9 @@ export class MediaDbAdvancedSearchModal extends Modal { this.searchBtn.setDisabled(false); this.searchBtn.setButtonText('Searching...'); - console.log(`MDB | query started with title ${this.query}`); - - const res = await this.plugin.apiManager.query(this.query, this.selectedApis); - this.onSubmit(null, res); + this.onSubmit({query: this.query, apis: apis}); } catch (e) { - this.onSubmit(e); + this.onSubmit(null, e); } finally { this.close(); } @@ -96,9 +88,9 @@ export class MediaDbAdvancedSearchModal extends Modal { const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); apiToggleComponent.setTooltip(api.apiName); - apiToggleComponent.setValue(this.selectedApis[api.apiName]); + apiToggleComponent.setValue(this.selectedApis.find(x => x.name === api.apiName).selected); apiToggleComponent.onChange((value) => { - this.selectedApis[api.apiName] = value; + this.selectedApis.find(x => x.name === api.apiName).selected = value; }); apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); } diff --git a/src/modals/MediaDbIdSearchModal.ts b/src/modals/MediaDbIdSearchModal.ts index 31c74d6..87fb4ce 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/src/modals/MediaDbIdSearchModal.ts @@ -9,9 +9,9 @@ export class MediaDbIdSearchModal extends Modal { plugin: MediaDbPlugin; searchBtn: ButtonComponent; selectedApi: string; - onSubmit: (err: Error, result?: MediaTypeModel) => void; + onSubmit: (res: {query: string, api: string}, err?: Error) => void; - constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (err: Error, result?: MediaTypeModel) => void) { + constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (res: {query: string, api: string}, err?: Error) => void) { super(app); this.plugin = plugin; this.onSubmit = onSubmit; @@ -44,16 +44,9 @@ export class MediaDbIdSearchModal extends Modal { this.searchBtn.setDisabled(false); this.searchBtn.setButtonText('Searching...'); - console.log(`MDB | query started with id ${this.query}`); - - const api = this.plugin.apiManager.getApiByName(this.selectedApi); - if (!api) { - this.onSubmit(new Error('the selected api does not exist')); - } - const res = await api.getById(this.query); - this.onSubmit(null, res); + this.onSubmit({query: this.query, api: this.selectedApi}); } catch (e) { - this.onSubmit(e); + this.onSubmit(null, e); } finally { this.close(); } diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index eeda417..e45a551 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -6,13 +6,13 @@ import {SelectModal} from './SelectModal'; export class MediaDbSearchResultModal extends SelectModal { plugin: MediaDbPlugin; heading: string; - onSubmit: (error: Error, result: MediaTypeModel[]) => void; + onSubmit: (res: MediaTypeModel[], err?: Error) => void; onCancel: () => void; onSkip: () => void; sendCallback: boolean; - constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], skipButton: boolean, onSubmit: (error: Error, result: MediaTypeModel[]) => void, onCancel: () => void, onSkip?: () => void) { + constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], skipButton: boolean, onSubmit: (res: MediaTypeModel[], err?: Error) => void, onCancel: () => void, onSkip?: () => void) { super(app, elements); this.plugin = plugin; this.onSubmit = onSubmit; @@ -35,7 +35,7 @@ export class MediaDbSearchResultModal extends SelectModal { // Perform action on the selected suggestion. submit() { - this.onSubmit(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); + this.onSubmit(this.selectModalElements.filter(x => x.isActive()).map(x => x.value)); this.sendCallback = true; this.close(); } diff --git a/src/settings/ModelPropertyMapper.ts b/src/settings/ModelPropertyMapper.ts index 5385ef4..39b8b51 100644 --- a/src/settings/ModelPropertyMapper.ts +++ b/src/settings/ModelPropertyMapper.ts @@ -19,17 +19,25 @@ export class ModelPropertyMapper { this.conversionRulesMap.set(MediaType.BoardGame, settings.boardgamePropertyConversionRules); } + /** + * Converts an object using the conversion rules for its type. + * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. + * + * @param obj + */ convertObject(obj: object): object { if (!obj.hasOwnProperty('type')) { return obj; } // @ts-ignore + // get conversion rules from settings corresponding to the object type const conversionRulesString: string = this.conversionRulesMap.get(obj['type']); if (!conversionRulesString) { return obj; } + // parse the conversion rules const conversionRules: ModelPropertyConversionRule[] = []; for (const conversionRuleString of conversionRulesString.split('\n')) { if (conversionRuleString) { @@ -39,8 +47,8 @@ export class ModelPropertyMapper { const newObj: object = {}; - for (const [key, value] of Object.entries(obj)) { + // property 'type' can not be remapped if (key === 'type') { // @ts-ignore newObj[key] = value; @@ -51,8 +59,11 @@ export class ModelPropertyMapper { for (const conversionRule of conversionRules) { if (conversionRule.property === key) { hasConversionRule = true; - // @ts-ignore - newObj[conversionRule.newProperty] = value; + // if the conversion rule maps to 'x', then that means it should be ignored + if (conversionRule.newProperty.toLowerCase() !== 'x') { + // @ts-ignore + newObj[conversionRule.newProperty] = value; + } } } if (!hasConversionRule) { @@ -64,18 +75,26 @@ export class ModelPropertyMapper { return newObj; } + /** + * Converts an object back using the conversion rules for its type. + * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. + * + * @param obj + */ convertObjectBack(obj: object): object { if (!obj.hasOwnProperty('type')) { return obj; } // @ts-ignore + // get conversion rules from settings corresponding to the object type const conversionRulesString: string = this.conversionRulesMap.get(obj['type']); if (!conversionRulesString) { return obj; } const conversionRules: ModelPropertyConversionRule[] = []; + // parse the conversion rules for (const conversionRuleString of conversionRulesString.split('\n')) { if (conversionRuleString) { conversionRules.push(new ModelPropertyConversionRule(conversionRuleString)); @@ -85,6 +104,7 @@ export class ModelPropertyMapper { const originalObj: object = {}; for (const [key, value] of Object.entries(obj)) { + // property 'type' can not be remapped if (key === 'type') { // @ts-ignore originalObj[key] = value; diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 529abae..9d576b0 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -58,6 +58,12 @@ export class MediaTypeManager { return replaceTags(template, mediaTypeModel); } + /** + * Takes an object and a MediaType and turns the object into an instance of a MediaTypeModel corresponding to the MediaType passed in. + * + * @param obj + * @param mediaType + */ createMediaTypeModelFromMediaType(obj: any, mediaType: MediaType): MediaTypeModel { if (mediaType === MediaType.Movie) { return new MovieModel(obj); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 399b223..e37a28c 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -5,7 +5,7 @@ export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; export const mediaDbVersion: string = '0.3.2'; -export const debug: boolean = false; +export const debug: boolean = true; export function wrapAround(value: number, size: number): number { return ((value % size) + size) % size;