diff --git a/README.md b/README.md index bf1c232..f1f5aee 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,15 @@ Now you select the result you want and the plugin will cast it's magic and creat You are more than welcome to open an issue on [GitHub](https://github.com/mProjectsCode/obsidian-media-db-plugin/issues). ### Changelog + +#### 0.3.2 +- Added Board Game Geek API (documentation pending) +- More information in the search results +- various fixes + +#### 0.3.1 +- various fixes + #### 0.3.0 - Added bulk import. Import a folder of media notes as Media DB entries (thanks to [PaperOrb](https://github.com/PaperOrb) on GitHub for their input and for helping me test this feature) - Added a custom result select modal that allows you to select multiple results at once diff --git a/manifest.json b/manifest.json index 17fbf0d..93a8ad3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB Plugin", - "version": "0.3.1", + "version": "0.3.2", "minAppVersion": "0.14.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts new file mode 100644 index 0000000..174bbff --- /dev/null +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -0,0 +1,104 @@ +import {APIModel} from '../APIModel'; +import {MediaTypeModel} from '../../models/MediaTypeModel'; +import MediaDbPlugin from '../../main'; +import {BoardGameModel} from 'src/models/BoardGameModel'; +import {debugLog} from '../../utils/Utils'; +import {requestUrl} from 'obsidian'; +import {MediaType} from '../../utils/MediaType'; + +export class BoardGameGeekAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'BoardGameGeekAPI'; + this.apiDescription = 'A free API for BoardGameGeek things.'; + this.apiUrl = 'https://api.geekdo.com/xmlapi'; + this.types = ['boardgames']; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from an API.`); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, "text/xml") + + debugLog(response); + + let ret: MediaTypeModel[] = []; + + for (const boardgame of Array.from(response.querySelectorAll("boardgame"))) { + const id = boardgame.attributes.getNamedItem("objectid")!.value; + const title = boardgame.querySelector("name")!.textContent!; + const year = boardgame.querySelector("yearpublished")?.textContent ?? ""; + + ret.push(new BoardGameModel({ + dataSource: this.apiName, + id, + title, + englishTitle: title, + year, + } as BoardGameModel)); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from an API.`); + } + + const data = fetchData.text; + const response = new window.DOMParser().parseFromString(data, "text/xml") + debugLog(response); + + const boardgame = response.querySelector("boardgame")!; + const title = boardgame.querySelector("name")!.textContent!; + const year = boardgame.querySelector("yearpublished")?.textContent ?? ""; + const image = boardgame.querySelector("image")?.textContent ?? undefined; + const onlineRating = Number.parseFloat(boardgame.querySelector("statistics ratings average")?.textContent ?? ""); + const genres = Array.from(boardgame.querySelectorAll("boardgamecategory")).map(n => n!.textContent!); + + const model = new BoardGameModel({ + type: MediaType.BoardGame, + title, + englishTitle: title, + year: year === "0" ? "" : year, + dataSource: this.apiName, + url: `https://boardgamegeek.com/boardgame/${id}`, + id, + + genres, + onlineRating, + image, + released: true, + + userData: { + played: false, + personalRating: 0, + }, + } as BoardGameModel); + + return model; + + } +} diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index faeb7aa..c51095d 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.ts @@ -100,7 +100,7 @@ export class SteamAPI extends APIModel { englishTitle: result.name, year: (new Date(result.release_date.date)).getFullYear().toString(), dataSource: this.apiName, - url: `https://store.steampowered.com/app/${result.id}`, + url: `https://store.steampowered.com/app/${result.steam_appid}`, id: result.steam_appid, genres: result.genres?.map((x: any) => x.description) ?? [], diff --git a/src/main.ts b/src/main.ts index c43124d..2196f27 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import {WikipediaAPI} from './api/apis/WikipediaAPI'; import {MusicBrainzAPI} from './api/apis/MusicBrainzAPI'; import {MediaTypeManager} from './utils/MediaTypeManager'; import {SteamAPI} from './api/apis/SteamAPI'; +import {BoardGameGeekAPI} from './api/apis/BoardGameGeekAPI'; import {ModelPropertyMapper} from './settings/ModelPropertyMapper'; import {YAMLConverter} from './utils/YAMLConverter'; import {MediaDbFolderImportModal} from './modals/MediaDbFolderImportModal'; @@ -79,6 +80,7 @@ export default class MediaDbPlugin extends Plugin { 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); diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index ec7015a..eeda417 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -29,7 +29,7 @@ export class MediaDbSearchResultModal extends SelectModal { // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement) { el.createEl('div', {text: this.plugin.mediaTypeManager.getFileName(item)}); - el.createEl('small', {text: `${item.englishTitle}\n`}); + el.createEl('small', {text: `${item.getSummary()}\n`}); el.createEl('small', {text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}`}); } diff --git a/src/models/BoardGameModel.ts b/src/models/BoardGameModel.ts new file mode 100644 index 0000000..a2ffad3 --- /dev/null +++ b/src/models/BoardGameModel.ts @@ -0,0 +1,39 @@ +import {MediaTypeModel} from './MediaTypeModel'; +import {mediaDbTag} from '../utils/Utils'; +import {MediaType} from '../utils/MediaType'; + + +export class BoardGameModel extends MediaTypeModel { + genres: string[]; + onlineRating: number; + image?: string; + + released: boolean; + + userData: { + played: boolean; + personalRating: number; + }; + + + constructor(obj: any = {}) { + super(); + + Object.assign(this, obj); + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'boardgame']; + } + + getMediaType(): MediaType { + return MediaType.BoardGame; + } + + getSummary(): string { + return this.englishTitle + ' (' + this.year + ')'; + } + +} diff --git a/src/models/GameModel.ts b/src/models/GameModel.ts index d19dc2e..2d2e68c 100644 --- a/src/models/GameModel.ts +++ b/src/models/GameModel.ts @@ -42,4 +42,8 @@ export class GameModel extends MediaTypeModel { return MediaType.Game; } + getSummary(): string { + return this.englishTitle + ' (' + this.year + ')'; + } + } diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index d82216d..f1c4bd2 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -14,6 +14,9 @@ export abstract class MediaTypeModel { abstract getMediaType(): MediaType; + //a string that contains enough info to disambiguate from similar media + abstract getSummary(): string; + abstract getTags(): string[]; toMetaDataObject(): object { diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index a761908..4300801 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -44,4 +44,7 @@ export class MovieModel extends MediaTypeModel { return MediaType.Movie; } + getSummary(): string { + return this.englishTitle + ' (' + this.year + ')'; + } } diff --git a/src/models/MusicReleaseModel.ts b/src/models/MusicReleaseModel.ts index 4094927..9aa03bf 100644 --- a/src/models/MusicReleaseModel.ts +++ b/src/models/MusicReleaseModel.ts @@ -37,4 +37,10 @@ export class MusicReleaseModel extends MediaTypeModel { return MediaType.MusicRelease; } + getSummary(): string { + var summary = this.title + ' (' + this.year + ')'; + if(this.artists.length > 0) + summary += ' - ' + this.artists.join(', ') + return summary; + } } diff --git a/src/models/SeriesModel.ts b/src/models/SeriesModel.ts index 5e49a59..43fcfae 100644 --- a/src/models/SeriesModel.ts +++ b/src/models/SeriesModel.ts @@ -47,4 +47,7 @@ export class SeriesModel extends MediaTypeModel { return MediaType.Series; } + getSummary(): string { + return this.title + ' (' + this.year + ')'; + } } diff --git a/src/models/WikiModel.ts b/src/models/WikiModel.ts index 64b2ab5..3121873 100644 --- a/src/models/WikiModel.ts +++ b/src/models/WikiModel.ts @@ -43,4 +43,7 @@ export class WikiModel extends MediaTypeModel { return copy; } + getSummary(): string { + return this.title; + } } diff --git a/src/settings/ModelPropertyMapper.ts b/src/settings/ModelPropertyMapper.ts index f9dc6ad..5385ef4 100644 --- a/src/settings/ModelPropertyMapper.ts +++ b/src/settings/ModelPropertyMapper.ts @@ -16,6 +16,7 @@ export class ModelPropertyMapper { this.conversionRulesMap.set(MediaType.Game, settings.gamePropertyConversionRules); this.conversionRulesMap.set(MediaType.Wiki, settings.wikiPropertyConversionRules); this.conversionRulesMap.set(MediaType.MusicRelease, settings.musicReleasePropertyConversionRules); + this.conversionRulesMap.set(MediaType.BoardGame, settings.boardgamePropertyConversionRules); } convertObject(obj: object): object { diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index e849809..b030311 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -18,18 +18,21 @@ export interface MediaDbPluginSettings { gameTemplate: string, wikiTemplate: string, musicReleaseTemplate: string, + boardgameTemplate: string, movieFileNameTemplate: string, seriesFileNameTemplate: string, gameFileNameTemplate: string, wikiFileNameTemplate: string, musicReleaseFileNameTemplate: string, + boardgameFileNameTemplate: string, moviePropertyConversionRules: string, seriesPropertyConversionRules: string, gamePropertyConversionRules: string, wikiPropertyConversionRules: string, musicReleasePropertyConversionRules: string, + boardgamePropertyConversionRules: string, } @@ -45,19 +48,21 @@ export const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameTemplate: '', wikiTemplate: '', musicReleaseTemplate: '', + boardgameTemplate: '', movieFileNameTemplate: '{{ title }} ({{ year }})', seriesFileNameTemplate: '{{ title }} ({{ year }})', gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})', + boardgameFileNameTemplate: '{{ title }} ({{ year }})', moviePropertyConversionRules: '', seriesPropertyConversionRules: '', gamePropertyConversionRules: '', wikiPropertyConversionRules: '', musicReleasePropertyConversionRules: '', - + boardgamePropertyConversionRules: '', }; @@ -201,6 +206,19 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.saveSettings(); }); }); + + new Setting(containerEl) + .setName('Board Game template') + .setDesc('Template file to be used when creating a new note for a boardgame.') + .addSearch(cb => { + new FileSuggest(this.app, cb.inputEl); + cb.setPlaceholder('Example: boardgameTemplate.md') + .setValue(this.plugin.settings.boardgameTemplate) + .onChange(data => { + this.plugin.settings.boardgameTemplate = data; + this.plugin.saveSettings(); + }); + }); // endregion containerEl.createEl('h3', {text: 'File Name Settings'}); @@ -264,6 +282,18 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.saveSettings(); }); }); + + new Setting(containerEl) + .setName('Board Game file name template') + .setDesc('Template for the file name used when creating a new note for a boardgame.') + .addText(cb => { + cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.boardgameFileNameTemplate}`) + .setValue(this.plugin.settings.boardgameFileNameTemplate) + .onChange(data => { + this.plugin.settings.boardgameFileNameTemplate = data; + this.plugin.saveSettings(); + }); + }); // endregion containerEl.createEl('h3', {text: 'Property Mappings'}); @@ -327,6 +357,18 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.saveSettings(); }); }); + + new Setting(containerEl) + .setName('Board Game model property mappings') + .setDesc('Mappings for the property names of a boardgame.') + .addTextArea(cb => { + cb.setPlaceholder(`Example: \ntitle -> name\nyear -> releaseYear`) + .setValue(this.plugin.settings.boardgamePropertyConversionRules) + .onChange(data => { + this.plugin.settings.boardgamePropertyConversionRules = data; + this.plugin.saveSettings(); + }); + }); // endregion } diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index c4f1076..c36680c 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -4,4 +4,5 @@ export enum MediaType { Game = 'game', MusicRelease = 'musicRelease', Wiki = 'wiki', + BoardGame = 'boardgame', } diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index ba0bd90..529abae 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -8,6 +8,7 @@ import {SeriesModel} from '../models/SeriesModel'; import {GameModel} from '../models/GameModel'; import {WikiModel} from '../models/WikiModel'; import {MusicReleaseModel} from '../models/MusicReleaseModel'; +import {BoardGameModel} from '../models/BoardGameModel'; export class MediaTypeManager { mediaFileNameTemplateMap: Map; @@ -24,6 +25,7 @@ export class MediaTypeManager { this.mediaFileNameTemplateMap.set(MediaType.Game, settings.gameFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.BoardGame, settings.boardgameFileNameTemplate); this.mediaTemplateMap = new Map(); this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate); @@ -31,6 +33,7 @@ export class MediaTypeManager { this.mediaTemplateMap.set(MediaType.Game, settings.gameTemplate); this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate); this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); + this.mediaTemplateMap.set(MediaType.BoardGame, settings.boardgameTemplate); } getFileName(mediaTypeModel: MediaTypeModel): string { @@ -66,6 +69,8 @@ export class MediaTypeManager { return new WikiModel(obj); } else if (mediaType === MediaType.MusicRelease) { return new MusicReleaseModel(obj); + } else if (mediaType === MediaType.BoardGame) { + return new BoardGameModel(obj); } return undefined; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f0cd940..399b223 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -4,7 +4,7 @@ import {MediaTypeModel} from '../models/MediaTypeModel'; 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.1'; +export const mediaDbVersion: string = '0.3.2'; export const debug: boolean = false; export function wrapAround(value: number, size: number): number {