Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
2a120ca
Fix build errors
dsynkd Mar 29, 2026
cb5e77a
Added tabs for settings
dsynkd Mar 29, 2026
4bb1662
Refactor API keys to use Obsidian keychain manager
dsynkd Mar 29, 2026
e5efccf
Added Band and Album media types
dsynkd Mar 29, 2026
a334db3
Added File Tree for Songs option
dsynkd Mar 29, 2026
040a135
Merge comic and book tab, fix bug where property mappins were not dis…
dsynkd Mar 29, 2026
6427615
Move property mappings to each relevant tab / section and make it modal
dsynkd Mar 29, 2026
eec6d65
merge movie tabs
dsynkd Mar 29, 2026
43a94fd
make type, id and dataSource properties remappable
dsynkd Mar 29, 2026
5da2dfa
Make dataSource removable for music media
dsynkd Mar 29, 2026
4aa2e47
Make Type metadata adjustable
dsynkd Mar 29, 2026
33a8719
Make year number
dsynkd Mar 29, 2026
9d3de01
Fix wikilinks formation for Artists and Album, fix file name sanitize…
dsynkd Mar 29, 2026
0d38b5d
make property mapper auto save
dsynkd Mar 30, 2026
b14bfdd
Add setting for normalized title as Alias
dsynkd Mar 30, 2026
548f25a
Update obsidian module to latest version
dsynkd Mar 30, 2026
f160f93
Clean up code
dsynkd Mar 30, 2026
37d1127
Fix bug with secret storage
dsynkd Mar 31, 2026
66d5cd4
Fix lyrics extractor
dsynkd Mar 31, 2026
dc4e756
Extend title normalization
dsynkd Mar 31, 2026
8d26765
Filter out bootleg albums
dsynkd Mar 31, 2026
10599be
Add ISNI metadata field for bands
dsynkd Mar 31, 2026
e7da03e
Added spotify link to song metadata
dsynkd Mar 31, 2026
613c379
Make duration int, add budget and revenue
dsynkd Mar 31, 2026
313906a
Fix bug where cancelling an overwrite for an album would continue fet…
dsynkd Apr 1, 2026
39613f9
Rename Band to Artist
dsynkd Apr 1, 2026
2ac67e2
various fixes 2
saimdeniz Apr 1, 2026
286cfb0
varios fixes 1
saimdeniz Apr 1, 2026
124c511
bug fixes
saimdeniz Apr 1, 2026
6978f36
Delete package-lock.json
saimdeniz Apr 1, 2026
56dd5e1
fix
saimdeniz Apr 1, 2026
1026393
fixes
saimdeniz Apr 1, 2026
774fceb
Delete main.ts
saimdeniz Apr 1, 2026
45b27a2
Delete styles.css
saimdeniz Apr 1, 2026
aff5fab
Delete models directory
saimdeniz Apr 1, 2026
ac21a7f
Delete modals directory
saimdeniz Apr 1, 2026
ceb07d1
Delete utils directory
saimdeniz Apr 1, 2026
41203d8
Delete settings directory
saimdeniz Apr 1, 2026
53a99c1
Delete api directory
saimdeniz Apr 1, 2026
cfadb8d
another fix
saimdeniz Apr 1, 2026
3860c37
change: frontmatter property ordering
saimdeniz Apr 2, 2026
53418fb
Add files via upload
saimdeniz Apr 3, 2026
06c98d0
Merge pull request #1 from saimdeniz/remake
saimdeniz Apr 3, 2026
89e4b05
Add files via upload
saimdeniz Apr 3, 2026
35c8fd5
Add files via upload
saimdeniz Apr 3, 2026
3ca124a
Add files via upload
saimdeniz Apr 3, 2026
478103d
Add files via upload
saimdeniz Apr 3, 2026
837647e
Fix formatting in credits section of README
saimdeniz Apr 3, 2026
423da0d
Add files via upload
saimdeniz Apr 3, 2026
17320a0
Fix formatting in credits section of README
saimdeniz Apr 3, 2026
90775d6
Add files via upload
saimdeniz Apr 3, 2026
5a6939e
Add files via upload
saimdeniz Apr 3, 2026
e1b49d3
Add files via upload
saimdeniz Apr 3, 2026
abaf7a5
Add files via upload
saimdeniz Apr 3, 2026
c781ac0
Add files via upload
saimdeniz Apr 3, 2026
95418b5
Add files via upload
saimdeniz Apr 3, 2026
4dddc67
Delete src/api/geniusLyricsExtract.ts
saimdeniz Apr 3, 2026
b64bae9
Add files via upload
saimdeniz Apr 3, 2026
3504b46
fix
saimdeniz Apr 3, 2026
6e9e6f0
Fix: silent update toggle for bulk tools
saimdeniz Apr 4, 2026
77f337e
fix recreate function
saimdeniz Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,35 +44,35 @@ I also published my own templates [here](https://github.com/mProjectsCode/obsidi

The plugin offers a setting to automatically download the poster images for a new media, ensuring offline access. The images are saved as `type_title (year)` e.g. `movie_The Perfect Storm (2000)`, in a user-chosen folder.

#### Metadata field customization
#### Property Mapping & Customization

Allows you to rename the metadata fields this plugin generates through mappings. The mappings can be set in the plugin's settings.
The three options for mapping are:
The plugin allows you to completely reorganize and customize how metadata fields are generated into your Obsidian notes. In the plugin settings, you can edit mappings with the following granular controls:

- `default`: Keep the original name
- `remap`: Rename the property
- `remove`: Removes the property entirely
- **Mapping Types**: Choose to keep original names (`default`), rename properties (`remap`), or skip them entirely (`remove`).
- **Drag & Drop Ordering**: Easily drag and rearrange properties to dictate their exact output order in your frontmatter.
- **Wikilink**: Convert specific properties into safe `[[Wiki-links]]` directly within the frontmatter.
- **Pin to Bottom**: Pin specific properties to the absolute bottom of the frontmatter. If multiple properties are pinned, they strictly follow your drag-and-drop order.
- **Auto-Tagging**: Dynamically convert property values into Obsidian tags, complete with a custom nested prefix input box (e.g., configuring `genres` to generate `#genre/action`).

#### Bulk Import
#### Bulk Operations

The plugin allows you to import your preexisting media collection and upgrade it to Media DB entries.
The plugin offers powerful bulk actions accessible via right-clicking a folder or the left-side Ribbon:

##### Prerequisites
- **Bulk Download Images**: Automatically downloads and stores remote poster images locally for all notes in a folder.
- **Bulk Update Metadata**: Refresh API data for multiple existing notes at once.
- **Bulk Recreate Metadata**: Features two unique modes: **Reset** (completely wipes and reconstructs the metadata) and **Safe** (preserves your manual text/content while safely reorganizing properties to match your latest mapping layout).
- **Import Folder as Media**: Convert a folder of basic notes (e.g. from a CSV import) into rich Media DB entries by searching their titles.

The preexisting media notes must be inside a folder in your vault.
For the plugin to be able to query them, they need one metadata field that is used as the title the piece of media is searched by.
This can be achieved by, for example, using a `csv` import plugin to import an existing list from outside of Obsidian.
#### Auto-Tracker Engine

##### Importing
An automated tracking system that periodically checks for `airing` and `released` state changes of your ongoing media (Series, Games, Movies, etc.).

To start the import process, right-click on the folder and select the `Import folder as Media DB entries` option.
Then specify the API to search, if the current note content and metadata should be appended to the Media DB entry, and the name of the metadata field that contains the title of the piece of media.
- The tracker scans on Obsidian startup or can be triggered manually via the Ribbon icon or bulk menus.
- **Custom Statuses**: You can customize the exact keywords the plugin looks for (e.g., matching your personalized vocabulary for "Airing", "Released", or "Upcoming") in the Settings panel.

Then the plugin will go through every file in the folder and prompt you to select from the search results.
#### Intelligent Ghost Tag Purging

##### Post import

After all files have been imported or the import was canceled, you will find the new entries as well as an error report that contains any errors or skipped/canceled files in the folder specified in the setting of the plugin.
The plugin's granular Auto-Tag generator is strictly non-destructive. Whenever a piece of media updates (e.g., a game's genre changes on the API), the plugin intelligently calculates which old tags it previously auto-generated and safely removes only those, leaving any manual tags you typed yourself perfectly preserved!

### How to install

Expand Down Expand Up @@ -130,6 +130,8 @@ Now you select the result you want, and the plugin will cast its magic, creating
| Comic Vine | The Comic Vine API offers metadata for comic books | comicbooks | Yes, by making an account [here](https://comicvine.gamespot.com/login-signup/) and going to the [api section](https://comicvine.gamespot.com/api/) of the site | 200 requests per resource, per hour. There is also a velocity detection to prevent malicious use. If too many requests are made per second, you may receive temporary blocks to resources. | No |
| [VNDB](https://vndb.org/) | The VNDB API offers metadata for visual novels | games | No | 200 requests per 5 minutes | Yes |
| [Boardgame Geek](https://boardgamegeek.com) | The Boardgame Geek API offers metadata for boardgames | boardgames | Yes, by making an account [here](https://boardgamegeek.com/join/) and then [requesting an application token](https://boardgamegeek.com/applications) | Exact usage limits are still undetermined | No |
| [IGDB](https://www.igdb.com/) | IGDB is a community-driven game database offering rich metadata for video games. | games | Yes, requires a free Twitch Developer account. Create an app at [dev.twitch.tv](https://dev.twitch.tv/console/apps) to get a Client ID and Client Secret. | 4 requests per second | No |
| [RAWG](https://rawg.io/) | RAWG is one of the largest open video game databases with Metacritic scores and platform data. | games | Yes, get a free API key at [rawg.io/apidocs](https://rawg.io/apidocs) | None stated | No |

#### Notes

Expand Down
4 changes: 2 additions & 2 deletions automation/build/esbuild.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import builtins from 'builtin-modules';
import { builtinModules } from 'node:module';
import esbuild from 'esbuild';
import esbuildSvelte from 'esbuild-svelte';
import { sveltePreprocess } from 'svelte-preprocess';
Expand Down Expand Up @@ -26,7 +26,7 @@ const build = await esbuild.build({
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
...builtinModules,
],
format: 'cjs',
target: 'es2018',
Expand Down
2 changes: 1 addition & 1 deletion manifest-beta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "obsidian-media-db-plugin",
"name": "Media DB",
"version": "0.8.0-canary.20260129T112027",
"minAppVersion": "1.5.0",
"minAppVersion": "1.11.4",
"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",
"authorUrl": "https://www.moritzjung.dev",
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "obsidian-media-db-plugin",
"name": "Media DB",
"version": "0.8.0",
"minAppVersion": "1.5.0",
"minAppVersion": "1.11.4",
"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",
"authorUrl": "https://www.moritzjung.dev",
Expand Down
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,29 @@
"dev": "vite build --watch --mode development",
"build": "bun run tsc && vite build --mode production",
"tsc": "tsc -noEmit -skipLibCheck",
"test": "bun test",
"test:log": "LOG_TESTS=true bun test",
"test": "npm test",
"test:log": "LOG_TESTS=true npm test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint --max-warnings=0 --no-warn-ignored src/**",
"lint:fix": "eslint --max-warnings=0 --fix --no-warn-ignored src/**",
"check": "bun run format:check && bun run tsc && bun run lint",
"check:fix": "bun run format && bun run tsc && bun run lint:fix",
"release": "bun run automation/release.ts",
"stats": "bun run automation/stats.ts"
"check": "npm run format:check && npm run tsc && npm run lint",
"check:fix": "npm run format && npm run tsc && npm run lint:fix",
"release": "npm run automation/release.ts",
"stats": "npm run automation/stats.ts"
},
"keywords": [],
"author": "Moritz Jung",
"license": "GPL-3.0",
"devDependencies": {
"@happy-dom/global-registrator": "^18.0.1",
"@lemons_dev/parsinom": "^0.0.12",
"@popperjs/core": "^2.11.8",
"@types/bun": "^1.3.7",
"builtin-modules": "^5.0.0",
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-only-warn": "^1.1.0",
"iso-639-2": "^3.0.2",
"obsidian": "latest",
"obsidian": "^1.12.3",
"openapi-fetch": "^0.14.1",
"openapi-typescript": "^7.10.1",
"prettier": "^3.8.1",
Expand Down
17 changes: 11 additions & 6 deletions src/api/APIManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Notice } from 'obsidian';
import type { MediaTypeModel } from '../models/MediaTypeModel';
import type { MediaType } from '../utils/MediaType';
import type { APIModel } from './APIModel';

export class APIManager {
Expand Down Expand Up @@ -40,24 +41,28 @@ export class APIManager {
* @param item
*/
async queryDetailedInfo(item: MediaTypeModel): Promise<MediaTypeModel | undefined> {
return await this.queryDetailedInfoById(item.id, item.dataSource);
return await this.queryDetailedInfoById(item.id, item.dataSource, item.getMediaType());
}

/**
* Queries detailed info for an id from an API.
* MusicBrainz-backed notes use on-disk dataSource `MusicBrainz`; `mediaType` picks Artist vs release/song API.
*
* @param id
* @param apiName
* @param apiName Stored dataSource on the note, or an exact {@link APIModel.apiName} (e.g. bulk import / ID search).
* @param mediaType When set with a MusicBrainz family dataSource, selects which MusicBrainz API handles {@link getById}.
*/
async queryDetailedInfoById(id: string, apiName: string): Promise<MediaTypeModel | undefined> {
async queryDetailedInfoById(id: string, apiName: string, mediaType?: MediaType): Promise<MediaTypeModel | undefined> {
const effectiveApiName = apiName.trim() || apiName;

// Delegate to each registered API — APIs override canHandleDataSource() for special logic
for (const api of this.apis) {
if (api.apiName === apiName) {
if (api.canHandleDataSource(effectiveApiName, mediaType)) {
try {
return api.getById(id);
return await api.getById(id);
} catch (e) {
new Notice(`Error querying ${api.apiName}: ${e}`);
console.warn(e);

return undefined;
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/api/APIModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,22 @@ export abstract class APIModel {
hasTypeOverlap(types: MediaType[]): boolean {
return types.some(type => this.hasType(type));
}

canHandleDataSource(dataSource: string, _mediaType?: MediaType): boolean {
return this.apiName === dataSource;
}

/**
* Returns the wiki-link string for a given property value.
*
* @param value the raw string value to wrap
* @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/')
*/
wikilinkValueFor(value: string, folderPrefix: string): string {
const clean = value
.replace(/^\[\[(.*?)\]\]$/, '$1')
.split('|')
.pop()!;
return `[[${folderPrefix}${clean}|${clean}]]`;
}
}
82 changes: 82 additions & 0 deletions src/api/GeniusClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { requestUrl } from 'obsidian';
import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils';
import { extractLyricsFromGeniusHtml } from './helpers/geniusLyricsExtract';

interface GeniusSearchHit {
result: {
id: number;
title: string;
url: string;
primary_artist: { name: string };
};
}

interface GeniusSearchResponse {
response: {
hits: GeniusSearchHit[];
};
}

export class GeniusClient {
private readonly accessToken: string | undefined;
private readonly userAgent: string;

constructor(accessToken: string | undefined) {
this.accessToken = accessToken;
this.userAgent = `${pluginName}/${mediaDbVersion} (${contactEmail})`;
}

isConfigured(): boolean {
return Boolean(this.accessToken?.trim());
}

async searchFirstSongHit(query: string): Promise<{ url: string; title: string } | null> {
if (!this.accessToken?.trim()) {
return null;
}

const url = `https://api.genius.com/search?q=${encodeURIComponent(query)}`;
const res = await requestUrl({
url,
throw: false,
headers: {
'User-Agent': this.userAgent,
Authorization: `Bearer ${this.accessToken.trim()}`,
},
});

if (res.status !== 200) {
if (res.status === 401) {
console.warn('MDB | Genius search returned 401 — access token missing, invalid, or expired. Update it in Media DB settings or clear it to skip lyrics.');
} else {
console.warn(`MDB | Genius search returned ${res.status}`);
}
return null;
}

const data = res.json as GeniusSearchResponse;
const hit = data.response?.hits?.[0]?.result;
if (!hit?.url) {
return null;
}

return { url: hit.url, title: hit.title };
}

async fetchLyricsFromSongPage(songPageUrl: string): Promise<string> {
const res = await requestUrl({
url: songPageUrl,
throw: false,
headers: {
'User-Agent': this.userAgent,
},
});

if (res.status !== 200) {
console.warn(`MDB | Genius song page returned ${res.status}`);
return '';
}

return extractLyricsFromGeniusHtml(res.text);
}
}
Loading