Skip to content

Commit bbc4b57

Browse files
committed
Bulk import #26
1 parent d34f107 commit bbc4b57

File tree

9 files changed

+421
-50
lines changed

9 files changed

+421
-50
lines changed

src/main.ts

Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {FrontMatterCache, Notice, Plugin, TFile} from 'obsidian';
1+
import {Notice, Plugin, TFile, TFolder} from 'obsidian';
22
import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings';
33
import {APIManager} from './api/APIManager';
44
import {MediaTypeModel} from './models/MediaTypeModel';
5-
import {replaceIllegalFileNameCharactersInString} from './utils/Utils';
5+
import {dateTimeToString, markdownTable, replaceIllegalFileNameCharactersInString} from './utils/Utils';
66
import {OMDbAPI} from './api/apis/OMDbAPI';
77
import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal';
88
import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal';
@@ -14,6 +14,7 @@ import {MediaTypeManager} from './utils/MediaTypeManager';
1414
import {SteamAPI} from './api/apis/SteamAPI';
1515
import {ModelPropertyMapper} from './settings/ModelPropertyMapper';
1616
import {YAMLConverter} from './utils/YAMLConverter';
17+
import {MediaDbFolderImportModal} from './modals/MediaDbFolderImportModal';
1718

1819
export default class MediaDbPlugin extends Plugin {
1920
settings: MediaDbPluginSettings;
@@ -30,6 +31,16 @@ export default class MediaDbPlugin extends Plugin {
3031
);
3132
ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class');
3233

34+
this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => {
35+
if (file instanceof TFolder) {
36+
menu.addItem(item => {
37+
item.setTitle('Create Media DB entries from folder')
38+
.setIcon('database')
39+
.onClick(() => this.createEntriesFromFolder(file as TFolder));
40+
});
41+
}
42+
}));
43+
3344
// register command to open search modal
3445
this.addCommand({
3546
id: 'open-media-db-search-modal',
@@ -74,16 +85,23 @@ export default class MediaDbPlugin extends Plugin {
7485
this.modelPropertyMapper = new ModelPropertyMapper(this.settings);
7586
}
7687

77-
async createMediaDbNote(modal: () => Promise<MediaTypeModel>): Promise<void> {
88+
async createMediaDbNote(modal: () => Promise<MediaTypeModel[]>): Promise<void> {
89+
let models: MediaTypeModel[] = [];
7890
try {
79-
let data: MediaTypeModel = await modal();
80-
data = await this.apiManager.queryDetailedInfo(data);
81-
82-
await this.createMediaDbNoteFromModel(data);
91+
models = await modal();
8392
} catch (e) {
8493
console.warn(e);
8594
new Notice(e.toString());
8695
}
96+
97+
for (const model of models) {
98+
try {
99+
await this.createMediaDbNoteFromModel(await this.apiManager.queryDetailedInfo(model));
100+
} catch (e) {
101+
console.warn(e);
102+
new Notice(e.toString());
103+
}
104+
}
87105
}
88106

89107
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel): Promise<void> {
@@ -126,44 +144,23 @@ export default class MediaDbPlugin extends Plugin {
126144
}
127145
}
128146

129-
async openMediaDbAdvancedSearchModal(): Promise<MediaTypeModel> {
130-
return new Promise(((resolve, reject) => {
131-
new MediaDbAdvancedSearchModal(this.app, this, (err, results) => {
132-
if (err) return reject(err);
133-
new MediaDbSearchResultModal(this.app, this, results, (err2, res) => {
134-
if (err2) return reject(err2);
135-
resolve(res);
136-
}).open();
137-
}).open();
138-
}));
139-
}
140-
141-
async openMediaDbIdSearchModal(): Promise<MediaTypeModel> {
142-
return new Promise(((resolve, reject) => {
143-
new MediaDbIdSearchModal(this.app, this, (err, res) => {
144-
if (err) return reject(err);
145-
resolve(res);
146-
}).open();
147-
}));
148-
}
149-
150147
async updateActiveNote() {
151148
const activeFile: TFile = this.app.workspace.getActiveFile();
152149
if (!activeFile) {
153150
throw new Error('MDB | there is no active note');
154151
}
155152

156153
let metadata: any = this.app.metadataCache.getFileCache(activeFile).frontmatter;
154+
metadata = JSON.parse(JSON.stringify(metadata)); // deep copy
157155
delete metadata.position; // remove unnecessary data from the FrontMatterCache
158156
metadata = this.modelPropertyMapper.convertObjectBack(metadata);
159157

160-
console.log(metadata)
158+
console.log(metadata);
161159

162160
if (!metadata?.type || !metadata?.dataSource || !metadata?.id) {
163161
throw new Error('MDB | active note is not a Media DB entry or is missing metadata');
164162
}
165163

166-
167164
let oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type);
168165

169166
let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource);
@@ -178,6 +175,94 @@ export default class MediaDbPlugin extends Plugin {
178175
await this.createMediaDbNoteFromModel(newMediaTypeModel);
179176
}
180177

178+
async createEntriesFromFolder(folder: TFolder) {
179+
const erroredFiles: { filePath: string, error: string }[] = [];
180+
181+
const {selectedAPI, titleFieldName} = await new Promise((resolve, reject) => {
182+
new MediaDbFolderImportModal(this.app, this, ((selectedAPI, titleFieldName) => {
183+
resolve({selectedAPI, titleFieldName});
184+
})).open();
185+
});
186+
187+
const selectedAPIs = {};
188+
for (const api of this.apiManager.apis) {
189+
// @ts-ignore
190+
selectedAPIs[api.apiName] = api.apiName === selectedAPI;
191+
}
192+
193+
for (const child of folder.children) {
194+
if (child instanceof TFile) {
195+
const file = child as TFile;
196+
let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter;
197+
198+
let title = metadata[titleFieldName];
199+
if (!title) {
200+
erroredFiles.push({filePath: file.path, error: `metadata field \'${titleFieldName}\' not found or empty`});
201+
continue;
202+
}
203+
204+
let results: MediaTypeModel[] = [];
205+
try {
206+
results = await this.apiManager.query(title, selectedAPIs);
207+
} catch (e) {
208+
erroredFiles.push({filePath: file.path, error: e.toString()});
209+
continue;
210+
}
211+
if (!results || results.length === 0) {
212+
erroredFiles.push({filePath: file.path, error: `no search results`});
213+
continue;
214+
}
215+
216+
let selectedResults: MediaTypeModel[] = await new Promise((resolve, reject) => {
217+
const searchResultModal = new MediaDbSearchResultModal(this.app, this, results, (err, res) => {
218+
if (err) return reject(err);
219+
resolve(res);
220+
});
221+
searchResultModal.title = `Results for \'${title}\'`;
222+
searchResultModal.open();
223+
});
224+
225+
await this.createMediaDbNote(async () => selectedResults);
226+
}
227+
}
228+
229+
if (erroredFiles.length > 0) {
230+
const title = `bulk import error report ${dateTimeToString(new Date())}`;
231+
const filePath = `${this.settings.folder.replace(/\/$/, '')}/${title}.md`;
232+
233+
const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error]));
234+
// console.log(table)
235+
let fileContent = `# ${title}\n\n${markdownTable(table)}`;
236+
237+
const targetFile = await this.app.vault.create(filePath, fileContent);
238+
}
239+
}
240+
241+
async openMediaDbAdvancedSearchModal(): Promise<MediaTypeModel[]> {
242+
return new Promise(((resolve, reject) => {
243+
new MediaDbAdvancedSearchModal(this.app, this, (err, results) => {
244+
if (err) {
245+
return reject(err);
246+
}
247+
new MediaDbSearchResultModal(this.app, this, results, (err2, res) => {
248+
if (err2) {
249+
return reject(err2);
250+
}
251+
resolve(res);
252+
}).open();
253+
}).open();
254+
}));
255+
}
256+
257+
async openMediaDbIdSearchModal(): Promise<MediaTypeModel> {
258+
return new Promise(((resolve, reject) => {
259+
new MediaDbIdSearchModal(this.app, this, (err, res) => {
260+
if (err) return reject(err);
261+
resolve(res);
262+
}).open();
263+
}));
264+
}
265+
181266
async loadSettings() {
182267
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
183268
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {App, ButtonComponent, DropdownComponent, Modal, Setting, TextComponent} from 'obsidian';
2+
import MediaDbPlugin from '../main';
3+
4+
export class MediaDbFolderImportModal extends Modal {
5+
plugin: MediaDbPlugin;
6+
onSubmit: (selectedAPI: string, titleFieldName: string) => void;
7+
selectedApi: string;
8+
searchBtn: ButtonComponent;
9+
titleFieldName: string;
10+
11+
constructor(app: App, plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, titleFieldName: string) => void) {
12+
super(app);
13+
this.plugin = plugin;
14+
this.onSubmit = onSubmit;
15+
this.selectedApi = plugin.apiManager.apis[0].apiName;
16+
}
17+
18+
submit() {
19+
this.onSubmit(this.selectedApi, this.titleFieldName);
20+
this.close();
21+
}
22+
23+
onOpen() {
24+
const {contentEl} = this;
25+
26+
contentEl.createEl('h2', {text: 'Create Media DB entries from folder'});
27+
28+
const apiSelectorWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'});
29+
const apiSelectorTextWrapper = apiSelectorWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'});
30+
apiSelectorTextWrapper.createEl('span', {text: 'API to search', cls: 'media-db-plugin-list-text'});
31+
32+
const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper);
33+
apiSelectorComponent.onChange((value: string) => {
34+
this.selectedApi = value;
35+
});
36+
for (const api of this.plugin.apiManager.apis) {
37+
apiSelectorComponent.addOption(api.apiName, api.apiName);
38+
}
39+
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);
40+
41+
42+
const placeholder = 'Title metadata field name';
43+
const titleFieldNameComponent = new TextComponent(contentEl);
44+
titleFieldNameComponent.inputEl.style.width = '100%';
45+
titleFieldNameComponent.setPlaceholder(placeholder);
46+
titleFieldNameComponent.onChange(value => this.titleFieldName = value);
47+
48+
contentEl.appendChild(titleFieldNameComponent.inputEl);
49+
50+
new Setting(contentEl)
51+
.addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close()))
52+
.addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit()));
53+
}
54+
55+
onClose() {
56+
const {contentEl} = this;
57+
contentEl.empty();
58+
}
59+
}
Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
1-
import {App, SuggestModal} from 'obsidian';
1+
import {App} from 'obsidian';
22
import {MediaTypeModel} from '../models/MediaTypeModel';
33
import MediaDbPlugin from '../main';
4+
import {SelectModal} from './SelectModal';
45

5-
export class MediaDbSearchResultModal extends SuggestModal<MediaTypeModel> {
6-
suggestion: MediaTypeModel[];
6+
export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
77
plugin: MediaDbPlugin;
8-
onChoose: (error: Error, result?: MediaTypeModel) => void;
8+
heading: string;
9+
onChoose: (error: Error, result: MediaTypeModel[]) => void;
910

10-
constructor(app: App, plugin: MediaDbPlugin, suggestion: MediaTypeModel[], onChoose: (error: Error, result?: MediaTypeModel) => void) {
11-
super(app);
11+
constructor(app: App, plugin: MediaDbPlugin, elements: MediaTypeModel[], onChoose: (error: Error, result: MediaTypeModel[]) => void) {
12+
super(app, elements);
1213
this.plugin = plugin;
13-
this.suggestion = suggestion;
1414
this.onChoose = onChoose;
15-
}
1615

17-
getSuggestions(query: string): MediaTypeModel[] {
18-
return this.suggestion.filter(item => {
19-
const searchQuery = query.toLowerCase();
20-
return item.title.toLowerCase().includes(searchQuery);
21-
});
16+
this.title = 'Search Results';
17+
this.description = 'Select one or multiple search results.';
2218
}
2319

2420
// Renders each suggestion item.
25-
renderSuggestion(item: MediaTypeModel, el: HTMLElement) {
21+
renderElement(item: MediaTypeModel, el: HTMLElement) {
2622
el.createEl('div', {text: this.plugin.mediaTypeManager.getFileName(item)});
2723
el.createEl('small', {text: `${item.englishTitle}\n`});
2824
el.createEl('small', {text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`});
2925
}
3026

3127
// Perform action on the selected suggestion.
32-
onChooseSuggestion(item: MediaTypeModel, evt: MouseEvent | KeyboardEvent) {
33-
this.onChoose(null, item);
28+
submit() {
29+
this.onChoose(null, this.selectModalElements.filter(x => x.isActive()).map(x => x.value));
30+
this.close();
3431
}
3532
}

src/modals/SelectModal.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {App, Modal, Setting} from 'obsidian';
2+
import {SelectModalElement} from './SelectModalElement';
3+
4+
export abstract class SelectModal<T> extends Modal {
5+
multiSelect: boolean;
6+
allowMultiSelect: boolean;
7+
8+
title: string;
9+
description: string;
10+
11+
elements: T[];
12+
selectModalElements: SelectModalElement<T>[];
13+
14+
15+
constructor(app: App, elements: T[]) {
16+
super(app);
17+
this.elements = elements;
18+
this.allowMultiSelect = true;
19+
20+
this.selectModalElements = [];
21+
}
22+
23+
abstract renderElement(value: T, el: HTMLElement): any;
24+
25+
abstract submit(): void;
26+
27+
disableAllOtherElements(elementId: number) {
28+
for (const selectModalElement of this.selectModalElements) {
29+
if (selectModalElement.id !== elementId) {
30+
selectModalElement.setActive(false);
31+
}
32+
}
33+
}
34+
35+
async onOpen() {
36+
const {contentEl} = this;
37+
38+
contentEl.createEl('h2', {text: this.title});
39+
contentEl.createEl('p', {text: this.description});
40+
41+
if (this.allowMultiSelect) {
42+
new Setting(contentEl)
43+
.setName('Select Multiple')
44+
.addToggle(cb => {
45+
cb.setValue(this.multiSelect);
46+
cb.onChange(value => {
47+
this.multiSelect = value;
48+
for (const selectModalElement of this.selectModalElements) {
49+
selectModalElement.setActive(false);
50+
}
51+
});
52+
});
53+
}
54+
55+
const elementWrapper = contentEl.createDiv({cls: 'media-db-plugin-select-wrapper'});
56+
57+
let i = 0;
58+
for (const element of this.elements) {
59+
const selectModalElement = new SelectModalElement(element, contentEl, i, this, false);
60+
61+
this.selectModalElements.push(selectModalElement);
62+
63+
this.renderElement(element, selectModalElement.element);
64+
65+
i += 1;
66+
}
67+
68+
new Setting(contentEl)
69+
.addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close()))
70+
.addButton(btn => btn.setButtonText('Ok').setCta().onClick(() => this.submit()));
71+
}
72+
}

0 commit comments

Comments
 (0)