Merge pull request #15 from mProjectsCode/master

version 0.1.9
This commit is contained in:
Moritz Jung 2022-06-01 13:51:39 +02:00 committed by GitHub
commit 30208ea71a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 627 additions and 161 deletions

View file

@ -4,32 +4,60 @@ A plugin that can query multiple APIs for movies, series, anime, games, music an
### Features
#### Search by Title
Search a movie, series, anime or game by its name across multiple APIs.
Search a movie, series, anime, game, music release or wiki article by its name across multiple APIs.
#### Search by ID
Allows you to search by an ID that varies from API to API. Concrete info can be found in the description of the individual APIs.
Allows you to search by an ID that varies from API to API. Concrete information on this feature can be found in the description of the individual APIs.
#### Templates
The plugin allows you to set a template note that gets added to the end of any note created by this plugin.
The plugin also offers simple "template tgs". E.g. if the template includes `{{ title }}`, it will be replaced by the title of the movie, show or game.
The plugin allows you to set a template note that gets added to the end of any note created by this plugin.
The plugin also offers simple "template tgs". E.g. if the template includes `{{ title }}`, it will be replaced by the title of the movie, show or game.
Note that "template tags" are surrounded with two curly braces and that the spaces inside the curly braces are important.
For arrays there are two special ways of displaying them.
- using `{{ LIST:variable_name }}` will result in
```
- element 1
- element 2
- element 3
- ...
```
```
- element 1
- element 2
- element 3
- ...
```
- using `{{ ENUM:variable_name }}` will result in
```
element 1, element 2, element 3, ...
```
```
element 1, element 2, element 3, ...
```
Available variables that can be used in template tags are the same variables from the metadata of the note.
I also published my own templates [here](https://github.com/mProjectsCode/obsidian-media-db-templates).
### How to install
Currently, you have to manually download the zip archive from the latest release here on GitHub.
After downloading, extract the archive into the `.obsidian/plugins` folder in your vault.
The folder structure should look like this:
```
[path to your vault]
|_ .obsidian
|_ plugins
|_ obsidian-media-db-plugin
|_ main.js
|_ manifest.json
|_ styles.css
```
Once the plugin submission goes through, the plugin will also be installable directly through obsidian's plugin installer.
### How to use
(pictures are coming)
Once you have installed this plugin, you will find a database icon in the left ribbon.
When using this or the `Add new Media DB entry` command, a popup will open.
Here you can enter the title of what you want to search for and then select in which APIs to search.
After clicking search, a new popup will open prompting you to select from the search results.
Now you select the result you want and the plugin will cast it's magic and create a new note in your vault, that contains the metadata of the selected search result.
### Currently supported media types
- movies (including specials)
- series (including OVAs)
@ -39,11 +67,12 @@ I also published my own templates [here](https://github.com/mProjectsCode/obsidi
### Currently supported APIs:
| Name | Description | Supported formats | Authentification | Rate limiting | SFW filter support |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------- | ------------------------------ | ------------------ |
|------------------------------------------------------|---------------------------------------------------------------------------------------------------|--------------------------------|------------------------------------------------------------------------------|--------------------------------|--------------------|
| [Jikan](https://jikan.moe/) | Jikan is an API that uses [My Anime List](https://myanimelist.net) and offers metadata for anime. | series, movies, specials, OVAs | No | 60 per minute and 3 per second | Yes |
| [OMDb](https://www.omdbapi.com/) | OMDb is an API that offers metadata for movie, series and games. | series, movies, games | Yes, you can get a free key here [here](https://www.omdbapi.com/apikey.aspx) | 1000 per day | No |
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows acces to all Wikipedia articles. | wiki articles | No | None | No | | | | | | |
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No |
#### Notes
- [Jikan](https://jikan.moe/)
@ -61,9 +90,15 @@ I also published my own templates [here](https://github.com/mProjectsCode/obsidi
- you can find this ID in the URL
- e.g. for "Rogue One" the URL looks like this `https://www.imdb.com/title/tt3748528/` so the ID is `tt3748528`
- [MusicBrainz](https://musicbrainz.org/)
- the id of a release is not easily accessibe, you are better of just searching by title
- the id of a release is not easily accessible, you are better off just searching by title
- [Wikipedia](https://en.wikipedia.org/wiki/Main_Page)
- [here](https://en.wikipedia.org/wiki/Wikipedia:Finding_a_Wikidata_ID) is a guide to finding the Wikipedia ID for an article
- [Steam](https://store.steampowered.com/)
- you can find this ID in the URL
- e.g. for "Factorio" the URL looks like this `https://store.steampowered.com/app/427520/Factorio/` so the ID is `427520`
### Problems, unexpected behavior or improvement suggestions?
You are more than welcome to open an issue on [GitHub](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues).
### Contributions
Thank you for wanting to contribute to this project.

View file

@ -25,9 +25,13 @@ export class APIManager {
}
async queryDetailedInfo(item: MediaTypeModel): Promise<MediaTypeModel> {
return await this.queryDetailedInfoById(item.id, item.dataSource);
}
async queryDetailedInfoById(id: string, dataSource: string): Promise<MediaTypeModel> {
for (const api of this.apis) {
if (api.apiName === item.dataSource) {
return api.getById(item);
if (api.apiName === dataSource) {
return api.getById(id);
}
}
}

View file

@ -13,7 +13,7 @@ export abstract class APIModel {
*/
abstract searchByTitle(title: string): Promise<MediaTypeModel[]>;
abstract getById(item: MediaTypeModel): Promise<MediaTypeModel>;
abstract getById(id: string): Promise<MediaTypeModel>;
hasType(type: string): boolean {
return this.types.contains(type);

View file

@ -40,10 +40,10 @@ export class LocGovAPI extends APIModel {
// return ret;
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://www.loc.gov/item/${item.id}/?fo=json`;
const searchUrl = `https://www.loc.gov/item/${encodeURIComponent(id)}/?fo=json`;
const fetchData = await fetch(searchUrl);
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from an API.`);

View file

@ -69,10 +69,10 @@ export class MALAPI extends APIModel {
return ret;
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://api.jikan.moe/v4/anime/${item.id}`;
const searchUrl = `https://api.jikan.moe/v4/anime/${encodeURIComponent(id)}`;
const fetchData = await fetch(searchUrl);
if (fetchData.status !== 200) {
@ -107,9 +107,11 @@ export class MALAPI extends APIModel {
released: true,
premiere: (new Date(result.aired?.from)).toLocaleDateString() ?? 'unknown',
watched: false,
lastWatched: '',
personalRating: 0,
userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
} as MovieModel);
return model;
@ -135,9 +137,11 @@ export class MALAPI extends APIModel {
airedTo: (new Date(result.aired?.to)).toLocaleDateString() ?? 'unknown',
airing: result.airing,
watched: false,
lastWatched: '',
personalRating: 0,
userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
} as SeriesModel);
return model;

View file

@ -58,14 +58,14 @@ export class MusicBrainzAPI extends APIModel {
return ret;
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(item.id)}?inc=releases+artists+tags+ratings+genres&fmt=json`;
const searchUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`;
const fetchData = await requestUrl({
url: searchUrl,
headers: {
'User-Agent': `${pluginName}/0.1.7 (${contactEmail})`,
'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`,
},
});
@ -90,6 +90,10 @@ export class MusicBrainzAPI extends APIModel {
genres: result.genres.map((g: any) => g.name),
subType: result['primary-type'],
rating: result.rating.value * 2,
userData: {
personalRating: 0,
},
} as MusicReleaseModel);
return model;

View file

@ -88,10 +88,10 @@ export class OMDbAPI extends APIModel {
return ret;
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `http://www.omdbapi.com/?i=${item.id}&apikey=${this.plugin.settings.OMDbKey}`;
const searchUrl = `http://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`;
const fetchData = await fetch(searchUrl);
if (fetchData.status === 401) {
@ -132,9 +132,11 @@ export class OMDbAPI extends APIModel {
released: true,
premiere: (new Date(result.Released)).toLocaleDateString() ?? 'unknown',
watched: false,
lastWatched: '',
personalRating: 0,
userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
} as MovieModel);
return model;
@ -160,9 +162,11 @@ export class OMDbAPI extends APIModel {
airedFrom: (new Date(result.Released)).toLocaleDateString() ?? 'unknown',
airedTo: 'unknown',
watched: false,
lastWatched: '',
personalRating: 0,
userData: {
watched: false,
lastWatched: '',
personalRating: 0,
},
} as SeriesModel);
return model;
@ -183,8 +187,10 @@ export class OMDbAPI extends APIModel {
released: true,
releaseDate: (new Date(result.Released)).toLocaleDateString() ?? 'unknown',
played: false,
personalRating: 0,
userData: {
played: false,
personalRating: 0,
},
} as GameModel);
return model;

119
src/api/apis/SteamAPI.ts Normal file
View file

@ -0,0 +1,119 @@
import {APIModel} from '../APIModel';
import {MediaTypeModel} from '../../models/MediaTypeModel';
import MediaDbPlugin from '../../main';
import {GameModel} from '../../models/GameModel';
import {debugLog} from '../../utils/Utils';
import {requestUrl} from 'obsidian';
import {MediaType} from '../../utils/MediaType';
export class SteamAPI extends APIModel {
plugin: MediaDbPlugin;
typeMappings: Map<string, string>;
constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'SteamAPI';
this.apiDescription = 'A free API for all Steam games.';
this.apiUrl = 'http://www.steampowered.com/';
this.types = ['games'];
this.typeMappings = new Map<string, string>();
this.typeMappings.set('game', 'game');
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
console.log(`MDB | api "${this.apiName}" queried by Title`);
const searchUrl = `http://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json`;
const fetchData = await requestUrl({
url: searchUrl,
});
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from an API.`);
}
const data = await fetchData.json;
debugLog(data);
let filteredData = [];
for (const app of data.applist.apps) {
if (app.name.toLowerCase().includes(title.toLowerCase())) {
filteredData.push(app);
}
if (filteredData.length > 20) {
break;
}
}
let ret: MediaTypeModel[] = [];
for (const result of filteredData) {
ret.push(new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: '',
dataSource: this.apiName,
id: result.appid,
} as GameModel));
}
return ret;
}
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `http://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}`;
const fetchData = await requestUrl({
url: searchUrl,
});
if (fetchData.status !== 200) {
throw Error(`MDB | Received status code ${fetchData.status} from an API.`);
}
debugLog(await fetchData.json);
let result;
for (const [key, value] of Object.entries(await fetchData.json)) {
if (key == id) {
result = value.data;
}
}
if (!result) {
throw Error(`MDB | API returned invalid data.`);
}
debugLog(result);
const model = new GameModel({
type: MediaType.Game,
title: result.name,
englishTitle: result.name,
year: (new Date(result.release_date.date)).getFullYear().toString(),
dataSource: this.apiName,
url: `https://store.steampowered.com/app/${result.id}`,
id: result.steam_appid,
genres: result.genres?.map((x: any) => x.description) ?? [],
onlineRating: Number.parseFloat(result.metacritic?.score ?? 0),
image: result.header_image ?? '',
released: !result.release_date?.comming_soon,
releaseDate: (new Date(result.release_date?.date)).toLocaleDateString() ?? 'unknown',
userData: {
played: false,
personalRating: 0,
},
} as GameModel);
return model;
}
}

View file

@ -46,10 +46,10 @@ export class WikipediaAPI extends APIModel {
return ret;
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
async getById(id: string): Promise<MediaTypeModel> {
console.log(`MDB | api "${this.apiName}" queried by ID`);
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${item.id}&inprop=url&format=json&origin=*`;
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`;
const fetchData = await fetch(searchUrl);
if (fetchData.status !== 200) {
@ -71,6 +71,8 @@ export class WikipediaAPI extends APIModel {
wikiUrl: result.fullurl,
lastUpdated: (new Date(result.touched)).toLocaleDateString() ?? 'unknown',
length: result.length,
userData: {},
} as WikiModel);
return model;

View file

@ -1,8 +1,8 @@
import {Notice, Plugin, TFile} from 'obsidian';
import {FrontMatterCache, Notice, Plugin, TFile} from 'obsidian';
import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/Settings';
import {APIManager} from './api/APIManager';
import {MediaTypeModel} from './models/MediaTypeModel';
import {replaceIllegalFileNameCharactersInString, replaceTags} from './utils/Utils';
import {replaceIllegalFileNameCharactersInString} from './utils/Utils';
import {OMDbAPI} from './api/apis/OMDbAPI';
import {MediaDbAdvancedSearchModal} from './modals/MediaDbAdvancedSearchModal';
import {MediaDbSearchResultModal} from './modals/MediaDbSearchResultModal';
@ -10,17 +10,20 @@ import {MALAPI} from './api/apis/MALAPI';
import {MediaDbIdSearchModal} from './modals/MediaDbIdSearchModal';
import {WikipediaAPI} from './api/apis/WikipediaAPI';
import {MusicBrainzAPI} from './api/apis/MusicBrainzAPI';
import {MediaTypeManager} from './utils/MediaTypeManager';
import {SteamAPI} from './api/apis/SteamAPI';
export default class MediaDbPlugin extends Plugin {
settings: MediaDbPluginSettings;
apiManager: APIManager;
mediaTypeManager: MediaTypeManager;
async onload() {
await this.loadSettings();
// add icon to the left ribbon
const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', (evt: MouseEvent) =>
this.createMediaDbNote(this.openMediaDbSearchModal.bind(this)),
this.createMediaDbNote(this.openMediaDbAdvancedSearchModal.bind(this)),
);
ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class');
@ -28,7 +31,7 @@ export default class MediaDbPlugin extends Plugin {
this.addCommand({
id: 'open-media-db-search-modal',
name: 'Add new Media DB entry',
callback: () => this.createMediaDbNote(this.openMediaDbSearchModal.bind(this)),
callback: () => this.createMediaDbNote(this.openMediaDbAdvancedSearchModal.bind(this)),
});
// register command to open id search modal
this.addCommand({
@ -36,6 +39,20 @@ export default class MediaDbPlugin extends Plugin {
name: 'Add new Media DB entry by id',
callback: () => this.createMediaDbNote(this.openMediaDbIdSearchModal.bind(this)),
});
// register command to update the open note
this.addCommand({
id: 'update-media-db-note',
name: 'Update the open note, if it is a Media DB entry.',
checkCallback: (checking: boolean) => {
if (!this.app.workspace.getActiveFile()) {
return false;
}
if (!checking) {
this.updateActiveNote()
}
return true;
},
});
// register the settings tab
this.addSettingTab(new MediaDbSettingTab(this.app, this));
@ -47,45 +64,43 @@ export default class MediaDbPlugin extends Plugin {
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 LocGovAPI(this)); // TODO: parse data
this.mediaTypeManager = new MediaTypeManager(this.settings);
}
async createMediaDbNote(modal: () => Promise<MediaTypeModel>): Promise<void> {
try {
let data: MediaTypeModel = await modal();
console.log('MDB | Creating new note...');
data = await this.apiManager.queryDetailedInfo(data);
// console.log(data);
await this.createMediaDbNoteFromModel(data);
} catch (e) {
console.warn(e);
new Notice(e.toString());
}
}
let fileContent = `---\n${data.toMetaData()}---\n`;
async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel): Promise<void> {
try {
console.log('MDB | Creating new note...');
// console.log(mediaTypeModel);
let templateFile: TFile = null;
let fileContent = `---\n${mediaTypeModel.toMetaData()}---\n`;
if (data.type === 'movie' && this.settings.movieTemplate) {
templateFile = this.app.vault.getFiles().filter((f: TFile) => f.name === this.settings.movieTemplate).first();
} else if (data.type === 'series' && this.settings.seriesTemplate) {
templateFile = this.app.vault.getFiles().filter((f: TFile) => f.name === this.settings.seriesTemplate).first();
} else if (data.type === 'game' && this.settings.gameTemplate) {
templateFile = this.app.vault.getFiles().filter((f: TFile) => f.name === this.settings.gameTemplate).first();
} else if (data.type === 'wiki' && this.settings.wikiTemplate) {
templateFile = this.app.vault.getFiles().filter((f: TFile) => f.name === this.settings.wikiTemplate).first();
} else if (data.type === 'musicRelease' && this.settings.musicReleaseTemplate) {
templateFile = this.app.vault.getFiles().filter((f: TFile) => f.name === this.settings.musicReleaseTemplate).first();
if (this.settings.templates) {
fileContent += await this.mediaTypeManager.getContent(mediaTypeModel, this.app);
}
if (templateFile) {
let template = await this.app.vault.cachedRead(templateFile);
// console.log(template);
if (this.settings.templates) {
template = replaceTags(template, data);
}
fileContent += template;
}
const fileName = replaceIllegalFileNameCharactersInString(data.getFileName());
const fileName = replaceIllegalFileNameCharactersInString(this.mediaTypeManager.getFileName(mediaTypeModel));
const filePath = `${this.settings.folder.replace(/\/$/, '')}/${fileName}.md`;
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file) {
await this.app.vault.delete(file);
}
const targetFile = await this.app.vault.create(filePath, fileContent);
// open file
@ -95,17 +110,18 @@ export default class MediaDbPlugin extends Plugin {
return;
}
await activeLeaf.openFile(targetFile, {state: {mode: 'source'}});
} catch (e) {
console.warn(e);
new Notice(e.toString());
}
}
async openMediaDbSearchModal(): Promise<MediaTypeModel> {
async openMediaDbAdvancedSearchModal(): Promise<MediaTypeModel> {
return new Promise(((resolve, reject) => {
new MediaDbAdvancedSearchModal(this.app, this.apiManager, (err, results) => {
new MediaDbAdvancedSearchModal(this.app, this, (err, results) => {
if (err) return reject(err);
new MediaDbSearchResultModal(this.app, results, (err2, res) => {
new MediaDbSearchResultModal(this.app, this, results, (err2, res) => {
if (err2) return reject(err2);
resolve(res);
}).open();
@ -115,18 +131,46 @@ export default class MediaDbPlugin extends Plugin {
async openMediaDbIdSearchModal(): Promise<MediaTypeModel> {
return new Promise(((resolve, reject) => {
new MediaDbIdSearchModal(this.app, this.apiManager, (err, res) => {
new MediaDbIdSearchModal(this.app, this, (err, res) => {
if (err) return reject(err);
resolve(res);
}).open();
}));
}
async updateActiveNote() {
const activeFile: TFile = this.app.workspace.getActiveFile();
if (!activeFile) {
throw new Error('MDB | there is no active note');
}
let metadata: FrontMatterCache = this.app.metadataCache.getFileCache(activeFile).frontmatter;
if (!metadata?.type || !metadata?.dataSource || !metadata?.id) {
throw new Error('MDB | active note is not a Media DB entry or is missing metadata');
}
delete metadata.position; // remove unnecessary data from the FrontMatterCache
let oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type);
let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource);
if (!newMediaTypeModel) {
return;
}
newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData());
console.log('MDB | deleting old entry');
await this.app.vault.delete(activeFile);
await this.createMediaDbNoteFromModel(newMediaTypeModel);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
this.mediaTypeManager.updateTemplates(this.settings);
await this.saveData(this.settings);
}
}

View file

@ -1,22 +1,22 @@
import {App, ButtonComponent, Component, Modal, Notice, Setting, TextComponent, ToggleComponent} from 'obsidian';
import {MediaTypeModel} from '../models/MediaTypeModel';
import {APIManager} from '../api/APIManager';
import {debugLog} from '../utils/Utils';
import MediaDbPlugin from '../main';
export class MediaDbAdvancedSearchModal extends Modal {
query: string;
isBusy: boolean;
apiManager: APIManager;
plugin: MediaDbPlugin;
searchBtn: ButtonComponent;
selectedApis: any;
onSubmit: (err: Error, result?: MediaTypeModel[]) => void;
constructor(app: App, apiManager: APIManager, onSubmit?: (err: Error, result?: MediaTypeModel[]) => void) {
constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (err: Error, result?: MediaTypeModel[]) => void) {
super(app);
this.apiManager = apiManager;
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApis = [];
for (const api of this.apiManager.apis) {
for (const api of this.plugin.apiManager.apis) {
this.selectedApis[api.apiName] = false;
}
}
@ -56,7 +56,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
console.log(`MDB | query started with title ${this.query}`);
const res = await this.apiManager.query(this.query, this.selectedApis);
const res = await this.plugin.apiManager.query(this.query, this.selectedApis);
this.onSubmit(null, res);
} catch (e) {
this.onSubmit(e);
@ -84,7 +84,7 @@ export class MediaDbAdvancedSearchModal extends Modal {
contentEl.createEl('h3', {text: 'APIs to search'});
const apiToggleComponents: Component[] = [];
for (const api of this.apiManager.apis) {
for (const api of this.plugin.apiManager.apis) {
const apiToggleListElementWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'});
const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'});

View file

@ -1,19 +1,19 @@
import {App, ButtonComponent, DropdownComponent, Modal, Notice, Setting, TextComponent} from 'obsidian';
import {MediaTypeModel} from '../models/MediaTypeModel';
import {APIManager} from '../api/APIManager';
import {debugLog} from '../utils/Utils';
import MediaDbPlugin from '../main';
export class MediaDbIdSearchModal extends Modal {
query: string;
isBusy: boolean;
apiManager: APIManager;
plugin: MediaDbPlugin;
searchBtn: ButtonComponent;
selectedApi: string;
onSubmit: (err: Error, result?: MediaTypeModel) => void;
constructor(app: App, apiManager: APIManager, onSubmit?: (err: Error, result?: MediaTypeModel) => void) {
constructor(app: App, plugin: MediaDbPlugin, onSubmit?: (err: Error, result?: MediaTypeModel) => void) {
super(app);
this.apiManager = apiManager;
this.plugin = plugin;
this.onSubmit = onSubmit;
this.selectedApi = '';
}
@ -46,7 +46,7 @@ export class MediaDbIdSearchModal extends Modal {
console.log(`MDB | query started with id ${this.query}`);
const api = this.apiManager.getApiByName(this.selectedApi);
const api = this.plugin.apiManager.getApiByName(this.selectedApi);
if (!api) {
this.onSubmit(new Error('the selected api does not exist'));
}
@ -83,7 +83,7 @@ export class MediaDbIdSearchModal extends Modal {
apiSelectorComponent.onChange((value: string) => {
this.selectedApi = value;
});
for (const api of this.apiManager.apis) {
for (const api of this.plugin.apiManager.apis) {
apiSelectorComponent.addOption(api.apiName, api.apiName);
}
apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl);

View file

@ -1,12 +1,15 @@
import {App, SuggestModal} from 'obsidian';
import {MediaTypeModel} from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
export class MediaDbSearchResultModal extends SuggestModal<MediaTypeModel> {
suggestion: MediaTypeModel[];
plugin: MediaDbPlugin;
onChoose: (error: Error, result?: MediaTypeModel) => void;
constructor(app: App, suggestion: MediaTypeModel[], onChoose: (error: Error, result?: MediaTypeModel) => void) {
constructor(app: App, plugin: MediaDbPlugin, suggestion: MediaTypeModel[], onChoose: (error: Error, result?: MediaTypeModel) => void) {
super(app);
this.plugin = plugin;
this.suggestion = suggestion;
this.onChoose = onChoose;
}
@ -20,9 +23,9 @@ export class MediaDbSearchResultModal extends SuggestModal<MediaTypeModel> {
// Renders each suggestion item.
renderSuggestion(item: MediaTypeModel, el: HTMLElement) {
el.createEl('div', {text: item.getFileName()});
el.createEl('div', {text: this.plugin.mediaTypeManager.getFileName(item)});
el.createEl('small', {text: `${item.englishTitle}\n`});
el.createEl('small', {text: `${item.type.toUpperCase()} from ${item.dataSource}`});
el.createEl('small', {text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`});
}
// Perform action on the selected suggestion.

View file

@ -1,10 +1,11 @@
import {MediaTypeModel} from './MediaTypeModel';
import {stringifyYaml} from 'obsidian';
import {mediaDbTag} from '../utils/Utils';
import {MediaType} from '../utils/MediaType';
export class GameModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -19,26 +20,26 @@ export class GameModel extends MediaTypeModel {
released: boolean;
releaseDate: string;
played: boolean;
personalRating: number;
userData: {
played: boolean;
personalRating: number;
};
constructor(obj: any = {}) {
super();
Object.assign(this, obj);
}
toMetaData(): string {
return stringifyYaml({...this, tags: '#' + this.getTags().join('/')});
}
getFileName(): string {
return this.title + (this.year ? ` (${this.year})` : '');
this.type = this.getMediaType();
}
getTags(): string[] {
return [mediaDbTag, 'game'];
}
getMediaType(): MediaType {
return MediaType.Game;
}
}

View file

@ -1,5 +1,9 @@
import {MediaType} from '../utils/MediaType';
import {YAMLConverter} from '../utils/YAMLConverter';
export abstract class MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -7,9 +11,20 @@ export abstract class MediaTypeModel {
url: string;
id: string;
abstract toMetaData(): string;
userData: object;
abstract getFileName(): string;
abstract getMediaType(): MediaType;
abstract getTags(): string[];
toMetaData(): string {
return YAMLConverter.toYaml({...this.getWithOutUserData(), ...this.userData, tags: '#' + this.getTags().join('/')});
}
getWithOutUserData(): object {
const copy = JSON.parse(JSON.stringify(this));
delete copy.userData;
return copy;
}
}

View file

@ -1,10 +1,11 @@
import {MediaTypeModel} from './MediaTypeModel';
import {stringifyYaml} from 'obsidian';
import {mediaDbTag} from '../utils/Utils';
import {MediaType} from '../utils/MediaType';
export class MovieModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -21,27 +22,26 @@ export class MovieModel extends MediaTypeModel {
released: boolean;
premiere: string;
watched: boolean;
lastWatched: string;
personalRating: number;
userData: {
watched: boolean;
lastWatched: string;
personalRating: number;
};
constructor(obj: any = {}) {
super();
Object.assign(this, obj);
}
toMetaData(): string {
return stringifyYaml({...this, tags: '#' + this.getTags().join('/')});
}
getFileName(): string {
return this.title + (this.year ? ` (${this.year})` : '');
this.type = this.getMediaType();
}
getTags(): string[] {
return [mediaDbTag, 'tv', 'movie'];
}
getMediaType(): MediaType {
return MediaType.Movie;
}
}

View file

@ -1,10 +1,11 @@
import {MediaTypeModel} from './MediaTypeModel';
import {stringifyYaml} from 'obsidian';
import {mediaDbTag} from '../utils/Utils';
import {MediaType} from '../utils/MediaType';
export class MusicReleaseModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -14,27 +15,26 @@ export class MusicReleaseModel extends MediaTypeModel {
genres: string[];
artists: string[];
subType: string;
rating: number;
personalRating: number;
userData: {
personalRating: number;
};
constructor(obj: any = {}) {
super();
Object.assign(this, obj);
}
toMetaData(): string {
return stringifyYaml({...this, tags: '#' + this.getTags().join('/')});
}
getFileName(): string {
return this.title + ' (' + this.artists.join(', ') + ' - ' + this.year + ' - ' + this.subType + ')';
this.type = this.getMediaType();
}
getTags(): string[] {
return [mediaDbTag, 'music', 'album'];
return [mediaDbTag, 'music', this.subType];
}
getMediaType(): MediaType {
return MediaType.MusicRelease;
}
}

View file

@ -1,10 +1,11 @@
import {MediaTypeModel} from './MediaTypeModel';
import {stringifyYaml} from 'obsidian';
import {mediaDbTag} from '../utils/Utils';
import {MediaType} from '../utils/MediaType';
export class SeriesModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -24,27 +25,26 @@ export class SeriesModel extends MediaTypeModel {
airedFrom: string;
airedTo: string;
watched: boolean;
lastWatched: string;
personalRating: number;
userData: {
watched: boolean;
lastWatched: string;
personalRating: number;
};
constructor(obj: any = {}) {
super();
Object.assign(this, obj);
}
toMetaData(): string {
return stringifyYaml({...this, tags: '#' + this.getTags().join('/')});
}
getFileName(): string {
return this.title + ' (' + this.year + ')';
this.type = this.getMediaType();
}
getTags(): string[] {
return [mediaDbTag, 'tv', 'series'];
}
getMediaType(): MediaType {
return MediaType.Series;
}
}

View file

@ -1,10 +1,11 @@
import {MediaTypeModel} from './MediaTypeModel';
import {stringifyYaml} from 'obsidian';
import {mediaDbTag} from '../utils/Utils';
import {MediaType} from '../utils/MediaType';
export class WikiModel extends MediaTypeModel {
type: string;
subType: string;
title: string;
englishTitle: string;
year: string;
@ -16,23 +17,22 @@ export class WikiModel extends MediaTypeModel {
lastUpdated: string;
length: number;
userData: {};
constructor(obj: any = {}) {
super();
Object.assign(this, obj);
}
toMetaData(): string {
return stringifyYaml({...this, tags: '#' + this.getTags().join('/')});
}
getFileName(): string {
return this.title;
this.type = this.getMediaType();
}
getTags(): string[] {
return [mediaDbTag, 'wiki'];
}
getMediaType(): MediaType {
return MediaType.Wiki;
}
}

View file

@ -9,23 +9,39 @@ export interface MediaDbPluginSettings {
folder: string,
sfwFilter: boolean,
OMDbKey: string,
movieTemplate: string,
seriesTemplate: string,
gameTemplate: string,
wikiTemplate: string,
musicReleaseTemplate: string,
movieFileNameTemplate: string,
seriesFileNameTemplate: string,
gameFileNameTemplate: string,
wikiFileNameTemplate: string,
musicReleaseFileNameTemplate: string,
templates: boolean,
}
export const DEFAULT_SETTINGS: MediaDbPluginSettings = {
folder: '',
folder: 'Media DB',
sfwFilter: true,
OMDbKey: '',
movieTemplate: '',
seriesTemplate: '',
gameTemplate: '',
wikiTemplate: '',
musicReleaseTemplate: '',
movieFileNameTemplate: '{{ title }} ({{ year }})',
seriesFileNameTemplate: '{{ title }} ({{ year }})',
gameFileNameTemplate: '{{ title }} ({{ year }})',
wikiFileNameTemplate: '{{ title }}',
musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})',
templates: true,
};
@ -57,6 +73,29 @@ export class MediaDbSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
.setName('OMDb API key')
.setDesc('API key for "www.omdbapi.com".')
.addText(cb => {
cb.setPlaceholder('API key')
.setValue(this.plugin.settings.OMDbKey)
.onChange(data => {
this.plugin.settings.OMDbKey = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('SFW filter')
.setDesc('Only shows SFW results for APIs that offer filtering.')
.addToggle(cb => {
cb.setValue(this.plugin.settings.sfwFilter)
.onChange(data => {
this.plugin.settings.sfwFilter = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Resolve {{ tags }} in templates')
.setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.')
@ -68,6 +107,9 @@ export class MediaDbSettingTab extends PluginSettingTab {
});
});
containerEl.createEl('h3', {text: 'Template Settings'});
// region templates
new Setting(containerEl)
.setName('Movie template')
.setDesc('Template file to be used when creating a new note for a movie.')
@ -132,29 +174,71 @@ export class MediaDbSettingTab extends PluginSettingTab {
this.plugin.saveSettings();
});
});
// endregion
containerEl.createEl('h3', {text: 'File Name Settings'});
// region file name templates
new Setting(containerEl)
.setName('OMDb API key')
.setDesc('API key for "www.omdbapi.com".')
.addText(cb => {
cb.setPlaceholder('API key')
.setValue(this.plugin.settings.OMDbKey)
.setName('Movie file name template')
.setDesc('Template for the file name used when creating a new note for a movie.')
.addSearch(cb => {
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.movieFileNameTemplate}`)
.setValue(this.plugin.settings.movieFileNameTemplate)
.onChange(data => {
this.plugin.settings.OMDbKey = data;
this.plugin.settings.movieFileNameTemplate = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('SFW filter')
.setDesc('Only shows SFW results for APIs that offer filtering.')
.addToggle(cb => {
cb.setValue(this.plugin.settings.sfwFilter)
.setName('Series file name template')
.setDesc('Template for the file name used when creating a new note for a series.')
.addSearch(cb => {
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.seriesFileNameTemplate}`)
.setValue(this.plugin.settings.seriesFileNameTemplate)
.onChange(data => {
this.plugin.settings.sfwFilter = data;
this.plugin.settings.seriesFileNameTemplate = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Game file name template')
.setDesc('Template for the file name used when creating a new note for a game.')
.addSearch(cb => {
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.gameFileNameTemplate}`)
.setValue(this.plugin.settings.gameFileNameTemplate)
.onChange(data => {
this.plugin.settings.gameFileNameTemplate = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Wiki file name template')
.setDesc('Template for the file name used when creating a new note for a wiki entry.')
.addSearch(cb => {
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.wikiFileNameTemplate}`)
.setValue(this.plugin.settings.wikiFileNameTemplate)
.onChange(data => {
this.plugin.settings.wikiFileNameTemplate = data;
this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('Music Release file name template')
.setDesc('Template for the file name used when creating a new note for a music release.')
.addSearch(cb => {
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.musicReleaseFileNameTemplate}`)
.setValue(this.plugin.settings.musicReleaseFileNameTemplate)
.onChange(data => {
this.plugin.settings.musicReleaseFileNameTemplate = data;
this.plugin.saveSettings();
});
});
// endregion
}
}

27
src/tests/TestAPI.ts Normal file
View file

@ -0,0 +1,27 @@
import {APIModel} from '../api/APIModel';
import {MediaTypeModel} from '../models/MediaTypeModel';
import MediaDbPlugin from '../main';
export class TestAPI extends APIModel {
plugin: MediaDbPlugin;
constructor(plugin: MediaDbPlugin) {
super();
this.plugin = plugin;
this.apiName = 'TestAPI';
this.apiDescription = 'A test API for automated testing.';
this.apiUrl = '';
this.types = [];
}
async getById(item: MediaTypeModel): Promise<MediaTypeModel> {
return undefined;
}
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
return [] as MediaTypeModel[];
}
}

7
src/utils/MediaType.ts Normal file
View file

@ -0,0 +1,7 @@
export enum MediaType {
Movie = 'movie',
Series = 'series',
Game = 'game',
MusicRelease = 'musicRelease',
Wiki = 'wiki',
}

View file

@ -0,0 +1,73 @@
import {MediaDbPluginSettings} from '../settings/Settings';
import {MediaType} from './MediaType';
import {MediaTypeModel} from '../models/MediaTypeModel';
import {replaceTags} from './Utils';
import {App, TFile} from 'obsidian';
import {MovieModel} from '../models/MovieModel';
import {SeriesModel} from '../models/SeriesModel';
import {GameModel} from '../models/GameModel';
import {WikiModel} from '../models/WikiModel';
import {MusicReleaseModel} from '../models/MusicReleaseModel';
export class MediaTypeManager {
mediaFileNameTemplateMap: Map<MediaType, string>;
mediaTemplateMap: Map<MediaType, string>;
constructor(settings: MediaDbPluginSettings) {
this.updateTemplates(settings);
}
updateTemplates(settings: MediaDbPluginSettings) {
this.mediaFileNameTemplateMap = new Map<MediaType, string>();
this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate);
this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate);
this.mediaFileNameTemplateMap.set(MediaType.Game, settings.gameFileNameTemplate);
this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate);
this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate);
this.mediaTemplateMap = new Map<MediaType, string>();
this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate);
this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate);
this.mediaTemplateMap.set(MediaType.Game, settings.gameTemplate);
this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate);
this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate);
}
getFileName(mediaTypeModel: MediaTypeModel): string {
return replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType()), mediaTypeModel);
}
async getContent(mediaTypeModel: MediaTypeModel, app: App) {
const templateFileName = this.mediaTemplateMap.get(mediaTypeModel.getMediaType());
if (!templateFileName) {
return '';
}
const templateFile: TFile = app.vault.getFiles().filter((f: TFile) => f.name === templateFileName).first();
if (!templateFile) {
return '';
}
const template = await app.vault.cachedRead(templateFile);
// console.log(template);
return replaceTags(template, mediaTypeModel);
}
createMediaTypeModelFromMediaType(obj: any, mediaType: MediaType): MediaTypeModel {
if (mediaType === MediaType.Movie) {
return new MovieModel(obj);
} else if (mediaType === MediaType.Series) {
return new SeriesModel(obj);
} else if (mediaType === MediaType.Game) {
return new GameModel(obj);
} else if (mediaType === MediaType.Wiki) {
return new WikiModel(obj);
} else if (mediaType === MediaType.MusicRelease) {
return new MusicReleaseModel(obj);
}
return undefined;
}
}

View file

@ -7,7 +7,6 @@ export const mediaDbTag: string = 'mediaDB';
export const mediaDbVersion: string = '0.1.8';
export const debug: boolean = false;
export function wrapAround(value: number, size: number): number {
return ((value % size) + size) % size;
}

View file

@ -0,0 +1,39 @@
export class YAMLConverter {
static toYaml(obj: any): string {
let output = '';
for (const [key, value] of Object.entries(obj)) {
output += `${key}: ${YAMLConverter.toYamlString(value, 0)}\n`;
}
return output;
}
private static toYamlString(value: any, indentation: number): string {
if (typeof value === 'boolean') {
return value ? 'true' : 'false';
} else if (typeof value === 'number') {
return value.toString();
} else if (typeof value === 'string') {
return '"' + value + '"';
} else if (typeof value === 'object') {
let output = '';
if (Array.isArray(value)) {
for (const valueElement of value) {
output += `\n${YAMLConverter.calculateSpacing(indentation)} - ${YAMLConverter.toYamlString(valueElement, indentation + 1)}`;
}
} else {
for (const [objKey, objValue] of Object.entries(value)) {
output += `\n${YAMLConverter.calculateSpacing(indentation)} ${objKey}: ${YAMLConverter.toYamlString(objValue, indentation + 1)}`;
}
}
return output;
}
}
private static calculateSpacing(indentation: number): string {
return ' '.repeat(indentation * 4);
}
}