From 47d59278a67f2ab73b24c2682d02e56c1b030f7d Mon Sep 17 00:00:00 2001 From: Cedric Finance Date: Wed, 17 Aug 2022 23:05:49 +0200 Subject: [PATCH 1/3] Add BoardGameGeekAPI --- src/api/apis/BoardGameGeekAPI.ts | 104 ++++++++++++++++++++++++++++ src/main.ts | 2 + src/models/BoardGameModel.ts | 43 ++++++++++++ src/settings/ModelPropertyMapper.ts | 1 + src/settings/Settings.ts | 44 +++++++++++- src/utils/MediaType.ts | 1 + src/utils/MediaTypeManager.ts | 2 + 7 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 src/api/apis/BoardGameGeekAPI.ts create mode 100644 src/models/BoardGameModel.ts diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts new file mode 100644 index 0000000..34cdc7b --- /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; + 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, + 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/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/models/BoardGameModel.ts b/src/models/BoardGameModel.ts new file mode 100644 index 0000000..3f5c1e4 --- /dev/null +++ b/src/models/BoardGameModel.ts @@ -0,0 +1,43 @@ +import {MediaTypeModel} from './MediaTypeModel'; +import {mediaDbTag} from '../utils/Utils'; +import {MediaType} from '../utils/MediaType'; + + +export class BoardGameModel extends MediaTypeModel { + type: string; + title: string; + englishTitle: string; + year: string; + dataSource: string; + url: string; + id: string; + + 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; + } + +} 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..eeb596d 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -24,6 +24,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 +32,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 { From 3d4041cf16b6372863d26ecd1458e87929830a0c Mon Sep 17 00:00:00 2001 From: Cedric Finance Date: Thu, 18 Aug 2022 00:53:47 +0200 Subject: [PATCH 2/3] Fix issue with search results without yearpublished tag --- src/api/apis/BoardGameGeekAPI.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 34cdc7b..25385db 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -41,14 +41,14 @@ export class BoardGameGeekAPI extends APIModel { 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; + const year = boardgame.querySelector("yearpublished")?.textContent ?? ""; ret.push(new BoardGameModel({ dataSource: this.apiName, id, title, englishTitle: title, - year, + year, } as BoardGameModel)); } @@ -82,7 +82,7 @@ export class BoardGameGeekAPI extends APIModel { type: MediaType.BoardGame, title, englishTitle: title, - year, + year: year === "0" ? "" : year, dataSource: this.apiName, url: `https://boardgamegeek.com/boardgame/${id}`, id, From c4643ae6427d0bcc0b722ba577a1e6d08ae8b4d2 Mon Sep 17 00:00:00 2001 From: Cedric Finance Date: Sun, 28 Aug 2022 19:50:00 +0200 Subject: [PATCH 3/3] Cleanup and enhance types --- src/api/apis/BoardGameGeekAPI.ts | 16 ++++++++-------- src/models/BoardGameModel.ts | 10 +--------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 25385db..174bbff 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -39,8 +39,8 @@ export class BoardGameGeekAPI extends APIModel { 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 id = boardgame.attributes.getNamedItem("objectid")!.value; + const title = boardgame.querySelector("name")!.textContent!; const year = boardgame.querySelector("yearpublished")?.textContent ?? ""; ret.push(new BoardGameModel({ @@ -71,12 +71,12 @@ export class BoardGameGeekAPI extends APIModel { 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; - const onlineRating = Number.parseFloat(boardgame.querySelector("statistics ratings average").textContent); - const genres = Array.from(boardgame.querySelectorAll("boardgamecategory")).map(n => n.textContent); + 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, diff --git a/src/models/BoardGameModel.ts b/src/models/BoardGameModel.ts index 3f5c1e4..8406cea 100644 --- a/src/models/BoardGameModel.ts +++ b/src/models/BoardGameModel.ts @@ -4,17 +4,9 @@ import {MediaType} from '../utils/MediaType'; export class BoardGameModel extends MediaTypeModel { - type: string; - title: string; - englishTitle: string; - year: string; - dataSource: string; - url: string; - id: string; - genres: string[]; onlineRating: number; - image: string; + image?: string; released: boolean;