Skip to content

Commit ab69497

Browse files
Merge pull request #105 from ltctceplrm/master
Add book support using the OpenLibrary API
2 parents 5054b0f + 184d666 commit ab69497

File tree

7 files changed

+214
-6
lines changed

7 files changed

+214
-6
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Now you select the result you want and the plugin will cast it's magic and creat
107107
- games
108108
- music releases
109109
- wiki articles
110+
- books
110111
111112
### Currently supported APIs:
112113
@@ -117,6 +118,8 @@ Now you select the result you want and the plugin will cast it's magic and creat
117118
| [MusicBrainz](https://musicbrainz.org/) | MusicBrainz is an API that offers information about music releases. | music releases | No | 50 per second | No |
118119
| [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | The Wikipedia API allows access to all Wikipedia articles. | wiki articles | No | None | No |
119120
| [Steam](https://store.steampowered.com/) | The Steam API offers information on all steam games. | games | No | 10000 per day | No |
121+
| [Open Library](https://openlibrary.org) | The OpenLibrary API offers metadata for books | books | No | Cover access is rate-limited when not using CoverID or OLID by max 100 requests/IP every 5 minutes. This plugin uses OLID so there shouldn't be a rate limit. | No |
122+
120123
121124
#### Notes
122125
@@ -142,6 +145,11 @@ Now you select the result you want and the plugin will cast it's magic and creat
142145
- [Steam](https://store.steampowered.com/)
143146
- you can find this ID in the URL
144147
- e.g. for "Factorio" the URL looks like this `https://store.steampowered.com/app/427520/Factorio/` so the ID is `427520`
148+
- [Open Library](https://openlibrary.org)
149+
- The ID you need is the "work" ID and not the "book" ID, it needs to start with `/works/`. You can find this ID in the URL
150+
- e.g. for "Fantastic Mr. Fox" the URL looks like this `https://openlibrary.org/works/OL45804W` so the ID is `/works/OL45804W`
151+
- This URL is located near the top of the page above the title, see `An edition of Fantastic Mr Fox (1970) `
152+
145153
146154
### Problems, unexpected behavior or improvement suggestions?
147155

src/api/apis/OpenLibraryAPI.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { APIModel } from '../APIModel';
2+
import { MediaTypeModel } from '../../models/MediaTypeModel';
3+
import MediaDbPlugin from '../../main';
4+
import { BookModel } from 'src/models/BookModel';
5+
import { MediaType } from '../../utils/MediaType';
6+
7+
export class OpenLibraryAPI extends APIModel {
8+
plugin: MediaDbPlugin;
9+
10+
constructor(plugin: MediaDbPlugin) {
11+
super();
12+
13+
this.plugin = plugin;
14+
this.apiName = 'OpenLibraryAPI';
15+
this.apiDescription = 'A free API for books';
16+
this.apiUrl = 'https://openlibrary.org/';
17+
this.types = [MediaType.Book];
18+
}
19+
20+
async searchByTitle(title: string): Promise<MediaTypeModel[]> {
21+
console.log(`MDB | api "${this.apiName}" queried by Title`);
22+
23+
const searchUrl = `https://openlibrary.org/search.json?title=${encodeURIComponent(title)}`;
24+
25+
const fetchData = await fetch(searchUrl);
26+
console.debug(fetchData);
27+
if (fetchData.status !== 200) {
28+
throw Error(`MDB | Received status code ${fetchData.status} from an API.`);
29+
}
30+
const data = await fetchData.json();
31+
32+
console.debug(data);
33+
34+
const ret: MediaTypeModel[] = [];
35+
36+
for (const result of data.docs) {
37+
ret.push(
38+
new BookModel({
39+
title: result.title,
40+
englishTitle: result.title_english ?? result.title,
41+
year: result.first_publish_year,
42+
dataSource: this.apiName,
43+
id: result.key,
44+
} as BookModel)
45+
);
46+
}
47+
48+
return ret;
49+
}
50+
51+
async getById(id: string): Promise<MediaTypeModel> {
52+
console.log(`MDB | api "${this.apiName}" queried by ID`);
53+
54+
const searchUrl = `https://openlibrary.org/search.json?q=key:${encodeURIComponent(id)}`;
55+
const fetchData = await fetch(searchUrl);
56+
console.debug(fetchData);
57+
58+
if (fetchData.status !== 200) {
59+
throw Error(`MDB | Received status code ${fetchData.status} from an API.`);
60+
}
61+
62+
const data = await fetchData.json();
63+
console.debug(data);
64+
const result = data.docs[0];
65+
66+
const model = new BookModel({
67+
title: result.title,
68+
year: result.first_publish_year,
69+
dataSource: this.apiName,
70+
url: `https://openlibrary.org` + result.key,
71+
id: result.key,
72+
englishTitle: result.title_english ?? result.title,
73+
74+
author: result.author_name ?? 'unknown',
75+
pages: result.number_of_pages_median ?? 'unknown',
76+
onlineRating: Number.parseFloat(Number(result.ratings_average ?? 0).toFixed(2)),
77+
image: `https://covers.openlibrary.org/b/OLID/` + result.cover_edition_key + `-L.jpg` ?? '',
78+
79+
released: true,
80+
81+
userData: {
82+
read: false,
83+
lastRead: '',
84+
personalRating: 0,
85+
},
86+
} as BookModel);
87+
88+
return model;
89+
}
90+
}

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI';
1010
import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager';
1111
import { SteamAPI } from './api/apis/SteamAPI';
1212
import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI';
13+
import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI';
1314
import { PropertyMapper } from './settings/PropertyMapper';
1415
import { YAMLConverter } from './utils/YAMLConverter';
1516
import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal';
@@ -36,6 +37,7 @@ export default class MediaDbPlugin extends Plugin {
3637
this.apiManager.registerAPI(new MusicBrainzAPI(this));
3738
this.apiManager.registerAPI(new SteamAPI(this));
3839
this.apiManager.registerAPI(new BoardGameGeekAPI(this));
40+
this.apiManager.registerAPI(new OpenLibraryAPI(this));
3941
// this.apiManager.registerAPI(new LocGovAPI(this)); // TODO: parse data
4042

4143
this.mediaTypeManager = new MediaTypeManager();

src/models/BookModel.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MediaTypeModel } from './MediaTypeModel';
2+
import { mediaDbTag, migrateObject } from '../utils/Utils';
3+
import { MediaType } from '../utils/MediaType';
4+
5+
export class BookModel extends MediaTypeModel {
6+
author: string;
7+
pages: number;
8+
image: string;
9+
onlineRating: number;
10+
english_title: string;
11+
12+
released: boolean;
13+
14+
userData: {
15+
read: boolean;
16+
lastRead: string;
17+
personalRating: number;
18+
};
19+
20+
constructor(obj: any = {}) {
21+
super();
22+
23+
this.author = undefined;
24+
this.pages = undefined;
25+
this.image = undefined;
26+
this.onlineRating = undefined;
27+
28+
this.released = undefined;
29+
30+
this.userData = {
31+
read: undefined,
32+
lastRead: undefined,
33+
personalRating: undefined,
34+
};
35+
36+
migrateObject(this, obj, this);
37+
38+
if (!obj.hasOwnProperty('userData')) {
39+
migrateObject(this.userData, obj, this.userData);
40+
}
41+
42+
this.type = this.getMediaType();
43+
}
44+
45+
getTags(): string[] {
46+
return [mediaDbTag, 'book'];
47+
}
48+
49+
getMediaType(): MediaType {
50+
return MediaType.Book;
51+
}
52+
53+
getSummary(): string {
54+
return this.englishTitle + ' (' + this.year + ')';
55+
}
56+
}

src/settings/Settings.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,31 @@ export interface MediaDbPluginSettings {
2222
wikiTemplate: string;
2323
musicReleaseTemplate: string;
2424
boardgameTemplate: string;
25+
bookTemplate: string;
2526

2627
movieFileNameTemplate: string;
2728
seriesFileNameTemplate: string;
2829
gameFileNameTemplate: string;
2930
wikiFileNameTemplate: string;
3031
musicReleaseFileNameTemplate: string;
3132
boardgameFileNameTemplate: string;
33+
bookFileNameTemplate: string;
3234

3335
moviePropertyConversionRules: string;
3436
seriesPropertyConversionRules: string;
3537
gamePropertyConversionRules: string;
3638
wikiPropertyConversionRules: string;
3739
musicReleasePropertyConversionRules: string;
3840
boardgamePropertyConversionRules: string;
41+
bookPropertyConversionRules: string;
3942

4043
movieFolder: string;
4144
seriesFolder: string;
4245
gameFolder: string;
4346
wikiFolder: string;
4447
musicReleaseFolder: string;
4548
boardgameFolder: string;
49+
bookFolder: string;
4650

4751
propertyMappingModels: PropertyMappingModel[];
4852
}
@@ -60,27 +64,31 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = {
6064
wikiTemplate: '',
6165
musicReleaseTemplate: '',
6266
boardgameTemplate: '',
67+
bookTemplate: '',
6368

6469
movieFileNameTemplate: '{{ title }} ({{ year }})',
6570
seriesFileNameTemplate: '{{ title }} ({{ year }})',
6671
gameFileNameTemplate: '{{ title }} ({{ year }})',
6772
wikiFileNameTemplate: '{{ title }}',
6873
musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})',
6974
boardgameFileNameTemplate: '{{ title }} ({{ year }})',
75+
bookFileNameTemplate: '{{ title }} ({{ year }})',
7076

7177
moviePropertyConversionRules: '',
7278
seriesPropertyConversionRules: '',
7379
gamePropertyConversionRules: '',
7480
wikiPropertyConversionRules: '',
7581
musicReleasePropertyConversionRules: '',
7682
boardgamePropertyConversionRules: '',
83+
bookPropertyConversionRules: '',
7784

7885
movieFolder: 'Media DB/movies',
7986
seriesFolder: 'Media DB/series',
8087
gameFolder: 'Media DB/games',
8188
wikiFolder: 'Media DB/wiki',
8289
musicReleaseFolder: 'Media DB/music',
8390
boardgameFolder: 'Media DB/boardgames',
91+
bookFolder: 'Media DB/books',
8492

8593
propertyMappingModels: [],
8694
};
@@ -194,7 +202,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
194202
// region new file location
195203
new Setting(containerEl)
196204
.setName('Movie Folder')
197-
.setDesc('Where newly imported movies should be places.')
205+
.setDesc('Where newly imported movies should be placed.')
198206
.addSearch(cb => {
199207
new FolderSuggest(this.app, cb.inputEl);
200208
cb.setPlaceholder(DEFAULT_SETTINGS.movieFolder)
@@ -207,7 +215,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
207215

208216
new Setting(containerEl)
209217
.setName('Series Folder')
210-
.setDesc('Where newly imported series should be places.')
218+
.setDesc('Where newly imported series should be placed.')
211219
.addSearch(cb => {
212220
new FolderSuggest(this.app, cb.inputEl);
213221
cb.setPlaceholder(DEFAULT_SETTINGS.seriesFolder)
@@ -220,7 +228,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
220228

221229
new Setting(containerEl)
222230
.setName('Game Folder')
223-
.setDesc('Where newly imported games should be places.')
231+
.setDesc('Where newly imported games should be placed.')
224232
.addSearch(cb => {
225233
new FolderSuggest(this.app, cb.inputEl);
226234
cb.setPlaceholder(DEFAULT_SETTINGS.gameFolder)
@@ -233,7 +241,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
233241

234242
new Setting(containerEl)
235243
.setName('Wiki Folder')
236-
.setDesc('Where newly imported wiki articles should be places.')
244+
.setDesc('Where newly imported wiki articles should be placed.')
237245
.addSearch(cb => {
238246
new FolderSuggest(this.app, cb.inputEl);
239247
cb.setPlaceholder(DEFAULT_SETTINGS.wikiFolder)
@@ -246,7 +254,7 @@ export class MediaDbSettingTab extends PluginSettingTab {
246254

247255
new Setting(containerEl)
248256
.setName('Music Folder')
249-
.setDesc('Where newly imported music should be places.')
257+
.setDesc('Where newly imported music should be placed.')
250258
.addSearch(cb => {
251259
new FolderSuggest(this.app, cb.inputEl);
252260
cb.setPlaceholder(DEFAULT_SETTINGS.musicReleaseFolder)
@@ -269,6 +277,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
269277
this.plugin.saveSettings();
270278
});
271279
});
280+
new Setting(containerEl)
281+
.setName('Book Folder')
282+
.setDesc('Where newly imported books should be placed.')
283+
.addSearch(cb => {
284+
new FolderSuggest(this.app, cb.inputEl);
285+
cb.setPlaceholder(DEFAULT_SETTINGS.bookFolder)
286+
.setValue(this.plugin.settings.bookFolder)
287+
.onChange(data => {
288+
this.plugin.settings.bookFolder = data;
289+
this.plugin.saveSettings();
290+
});
291+
});
272292
// endregion
273293

274294
containerEl.createEl('h3', { text: 'Template Settings' });
@@ -350,6 +370,19 @@ export class MediaDbSettingTab extends PluginSettingTab {
350370
this.plugin.saveSettings();
351371
});
352372
});
373+
374+
new Setting(containerEl)
375+
.setName('Book template')
376+
.setDesc('Template file to be used when creating a new note for a book.')
377+
.addSearch(cb => {
378+
new FileSuggest(this.app, cb.inputEl);
379+
cb.setPlaceholder('Example: bookTemplate.md')
380+
.setValue(this.plugin.settings.bookTemplate)
381+
.onChange(data => {
382+
this.plugin.settings.bookTemplate = data;
383+
this.plugin.saveSettings();
384+
});
385+
});
353386
// endregion
354387

355388
containerEl.createEl('h3', { text: 'File Name Settings' });
@@ -425,6 +458,18 @@ export class MediaDbSettingTab extends PluginSettingTab {
425458
this.plugin.saveSettings();
426459
});
427460
});
461+
462+
new Setting(containerEl)
463+
.setName('Book file name template')
464+
.setDesc('Template for the file name used when creating a new note for a book.')
465+
.addText(cb => {
466+
cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.bookFileNameTemplate}`)
467+
.setValue(this.plugin.settings.bookFileNameTemplate)
468+
.onChange(data => {
469+
this.plugin.settings.bookFileNameTemplate = data;
470+
this.plugin.saveSettings();
471+
});
472+
});
428473
// endregion
429474

430475
// region Property Mappings

src/utils/MediaType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export enum MediaType {
55
MusicRelease = 'musicRelease',
66
Wiki = 'wiki',
77
BoardGame = 'boardgame',
8+
Book = 'book',
89
}

0 commit comments

Comments
 (0)