From 0bb17177eec964375f86a8e6555af2a5ca4a3fac Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:57:33 +0200 Subject: [PATCH] WIP season search --- README.md | 1 + src/api/apis/TMDBMovieAPI.ts | 130 ++++++++++++++++++++++++++ src/api/apis/TMDBSeasonAPI.ts | 169 ++++++++++++++++++++++++++++++++++ src/api/apis/TMDBSeriesAPI.ts | 131 ++++++++++++++++++++++++++ src/main.ts | 6 ++ src/models/SeasonModel.ts | 80 ++++++++++++++++ src/settings/Settings.ts | 54 +++++++++++ src/utils/MediaType.ts | 1 + src/utils/MediaTypeManager.ts | 7 ++ 9 files changed, 579 insertions(+) create mode 100644 src/api/apis/TMDBMovieAPI.ts create mode 100644 src/api/apis/TMDBSeasonAPI.ts create mode 100644 src/api/apis/TMDBSeriesAPI.ts create mode 100644 src/models/SeasonModel.ts diff --git a/README.md b/README.md index 08131e6..e877f81 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Now you select the result you want and the plugin will cast it's magic and creat | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | [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, manga, manwha, novels | 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 | +| [TMDB](https://www.themoviedb.org/) | TMDB is a API that offers community editable metadata for movies and series. | series, movies | Yes, by making an account [here](https://www.themoviedb.org/signup) and getting your `API Key` (**not** `API Read Access Token`) [here](https://www.themoviedb.org/settings/api) | 50 per second | Yes | | [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 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 | diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts new file mode 100644 index 0000000..71494d8 --- /dev/null +++ b/src/api/apis/TMDBMovieAPI.ts @@ -0,0 +1,130 @@ +import { Notice, renderResults } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MovieModel } from '../../models/MovieModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +export class TMDBMovieAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBMovieAPI'; + this.apiDescription = 'A community built Movie DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Movie]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + if (!this.plugin.settings.TMDBKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `https://api.themoviedb.org/3/search/movie?api_key=${this.plugin.settings.TMDBKey}&query=${encodeURIComponent(title)}&include_adult=${this.plugin.settings.sfwFilter ? 'false' : 'true'}`; + const fetchData = await fetch(searchUrl); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json(); + + if (data.total_results === 0) { + if (data.Error === 'Movie not found!') { + return []; + } + + throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`); + } + if (!data.results) { + return []; + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new MovieModel({ + type: 'movie', + title: result.original_title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + if (!this.plugin.settings.TMDBKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `https://api.themoviedb.org/3/movie/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`; + const fetchData = await fetch(searchUrl); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const result = await fetchData.json(); + // console.debug(result); + + return new MovieModel({ + type: 'movie', + title: result.title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/movie/${result.id}`, + id: result.id, + + plot: result.overview ?? '', + genres: result.genres.map((g: any) => g.name) ?? [], + writer: result.credits.crew.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [], + director: result.credits.crew.filter((c: any) => c.job === 'Director').map((c: any) => c.name) ?? [], + studio: result.production_companies.map((s: any) => s.name) ?? [], + + duration: result.runtime ?? 'unknown', + onlineRating: result.vote_average, + actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [], + image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, + + released: ['Released'].includes(result.status), + streamingServices: [], + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes as MediaType[]; + } +} diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts new file mode 100644 index 0000000..dab2fc2 --- /dev/null +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -0,0 +1,169 @@ +import { Notice, renderResults } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import { SeasonModel } from '../../models/SeasonModel'; + +export class TMDBSeasonAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'TMDBSeasonAPI'; + this.apiDescription = 'A community built Series DB (seasons).'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Season]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'season'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + if (!this.plugin.settings.TMDBKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + // 1) Search for series + const searchUrl = `https://api.themoviedb.org/3/search/tv?api_key=${this.plugin.settings.TMDBKey}&query=${encodeURIComponent(title)}&include_adult=${this.plugin.settings.sfwFilter ? 'false' : 'true'}`; + const searchResp = await fetch(searchUrl); + + if (searchResp.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (searchResp.status !== 200) { + throw Error(`MDB | Received status code ${searchResp.status} from ${this.apiName}.`); + } + + const searchData = await searchResp.json(); + + if (searchData.total_results === 0 || !searchData.results) { + return []; + } + + const ret: MediaTypeModel[] = []; + + // 2) For each series result, fetch its seasons and flatten into SeasonModel entries (cap total to 20) + for (const series of searchData.results) { + if (ret.length >= 20) break; + + const tvId = series.id; + const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}`; + const tvResp = await fetch(seriesUrl); + + if (tvResp.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (tvResp.status !== 200) { + // Skip this series if it fails; do not abort the whole search + console.warn(`MDB | Skipping series ${tvId} due to status ${tvResp.status}`); + continue; + } + + const tvData = await tvResp.json(); + const seriesName = tvData?.name ?? series?.name ?? series?.original_name ?? ''; + + if (Array.isArray(tvData?.seasons)) { + for (const season of tvData.seasons) { + if (ret.length >= 20) break; + + // Some seasons (e.g., specials) may have limited metadata; handle gracefully + const seasonNumber = season.season_number ?? 0; + const airDate = season.air_date ?? ''; + const titleText = `${seriesName} - Season ${seasonNumber}`; + ret.push( + new SeasonModel({ + // SeasonModel constructor sets type to MediaType.Series internally + title: titleText, + englishTitle: titleText, + year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: `${tvId}-S${seasonNumber}`, + seasonTitle: season.name ?? titleText, + seasonNumber: seasonNumber, + episodes: season.episode_count ?? 0, + airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', + airedTo: 'unknown', + plot: season.overview ?? '', + image: season.poster_path ? `https://image.tmdb.org/t/p/w780${season.poster_path}` : '', + userData: { watched: false, lastWatched: '', personalRating: 0 }, + }), + ); + } + } + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + if (!this.plugin.settings.TMDBKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + // Expect season ids like "12345-S2" + const m = /^(\d+)-S(\d+)$/.exec(id); + if (!m) { + throw Error(`MDB | Invalid season id "${id}". Expected format "-S".`); + } + + const tvId = m[1]; + const seasonNumber = m[2]; + + const seasonUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}/season/${encodeURIComponent(seasonNumber)}?api_key=${this.plugin.settings.TMDBKey}`; + const seasonResp = await fetch(seasonUrl); + + if (seasonResp.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (seasonResp.status !== 200) { + throw Error(`MDB | Received status code ${seasonResp.status} from ${this.apiName}.`); + } + + const seasonData = await seasonResp.json(); + + // Fetch parent series to build consistent titles + const seriesUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(tvId)}?api_key=${this.plugin.settings.TMDBKey}`; + const seriesResp = await fetch(seriesUrl); + + if (seriesResp.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (seriesResp.status !== 200) { + throw Error(`MDB | Received status code ${seriesResp.status} from ${this.apiName}.`); + } + + const seriesData = await seriesResp.json(); + const seriesName = seriesData?.name ?? ''; + + const airDate = seasonData.air_date ?? ''; + const titleText = `${seriesName} - Season ${seasonData.season_number}`; + + return new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: `${tvId}-S${seasonData.season_number}`, + seasonTitle: seasonData.name ?? titleText, + seasonNumber: seasonData.season_number ?? Number(seasonNumber), + episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : (seasonData.episodes ?? 0), + airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', + airedTo: 'unknown', + plot: seasonData.overview ?? '', + image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', + userData: { watched: false, lastWatched: '', personalRating: 0 }, + }); + } + + // Settings didn’t define TMDBSeasonAPIdisabledMediaTypes yet; return an empty list for now + getDisabledMediaTypes(): MediaType[] { + return []; + } +} diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts new file mode 100644 index 0000000..1fcf383 --- /dev/null +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -0,0 +1,131 @@ +import { Notice, renderResults } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { SeriesModel } from '../../models/SeriesModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +export class TMDBSeriesAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'TMDBSeriesAPI'; + this.apiDescription = 'A community built Series DB.'; + this.apiUrl = 'https://www.themoviedb.org/'; + this.types = [MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('tv', 'series'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + if (!this.plugin.settings.TMDBKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `https://api.themoviedb.org/3/search/tv?api_key=${this.plugin.settings.TMDBKey}&query=${encodeURIComponent(title)}&include_adult=${this.plugin.settings.sfwFilter ? 'false' : 'true'}`; + const fetchData = await fetch(searchUrl); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json(); + + if (data.total_results === 0) { + if (data.Error === 'Series not found!') { + return []; + } + + throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`); + } + if (!data.results) { + return []; + } + + // console.debug(data.results); + + const ret: MediaTypeModel[] = []; + + for (const result of data.results) { + ret.push( + new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + id: result.id, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + if (!this.plugin.settings.TMDBKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `https://api.themoviedb.org/3/tv/${encodeURIComponent(id)}?api_key=${this.plugin.settings.TMDBKey}&append_to_response=credits`; + const fetchData = await fetch(searchUrl); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const result = await fetchData.json(); + // console.debug(result); + + return new SeriesModel({ + type: 'series', + title: result.original_name, + englishTitle: result.name, + year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${result.id}`, + id: result.id, + + plot: result.overview ?? '', + genres: result.genres.map((g: any) => g.name) ?? [], + writer: result.created_by.map((c: any) => c.name) ?? [], + studio: result.production_companies.map((s: any) => s.name) ?? [], + episodes: result.number_of_episodes, + duration: result.episode_run_time[0] ?? 'unknown', + onlineRating: result.vote_average, + actors: result.credits.cast.map((c: any) => c.name).slice(0, 5) ?? [], + image: `https://image.tmdb.org/t/p/w780${result.poster_path}`, + + released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status), + streamingServices: [], + airing: ['Returning Series'].includes(result.status), + airedFrom: this.plugin.dateFormatter.format(result.first_air_date, this.apiDateFormat) ?? 'unknown', + airedTo: ['Returning Series'].includes(result.status) ? 'unknown' : (this.plugin.dateFormatter.format(result.last_air_date, this.apiDateFormat) ?? 'unknown'), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBSeriesAPI_disabledMediaTypes as MediaType[]; + } +} diff --git a/src/main.ts b/src/main.ts index 4c69dc4..1fcadc5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,9 @@ import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { SteamAPI } from './api/apis/SteamAPI'; +import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; +import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; +import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { MediaTypeModel } from './models/MediaTypeModel'; @@ -56,6 +59,9 @@ 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 TMDBSeriesAPI(this)); + this.apiManager.registerAPI(new TMDBSeasonAPI(this)); + this.apiManager.registerAPI(new TMDBMovieAPI(this)); this.apiManager.registerAPI(new BoardGameGeekAPI(this)); this.apiManager.registerAPI(new OpenLibraryAPI(this)); this.apiManager.registerAPI(new ComicVineAPI(this)); diff --git a/src/models/SeasonModel.ts b/src/models/SeasonModel.ts new file mode 100644 index 0000000..ef5a20a --- /dev/null +++ b/src/models/SeasonModel.ts @@ -0,0 +1,80 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type SeasonData = ModelToData; + +export class SeasonModel extends MediaTypeModel { + seasonNumber: number; + seasonTitle: string; + episodes: number; + + plot: string; + genres: string[]; + writer: string[]; + studio: string[]; + duration: string; + onlineRating: number; + actors: string[]; + image: string; + + released: boolean; + streamingServices: string[]; + airing: boolean; + airedFrom: string; + airedTo: string; + + userData: { + watched: boolean; + lastWatched: string; + personalRating: number; + }; + + constructor(obj: SeasonData) { + super(); + this.seasonTitle = ''; + this.seasonNumber = 0; + this.episodes = 0; + this.plot = ''; + this.genres = []; + this.writer = []; + this.studio = []; + this.duration = ''; + this.onlineRating = 0; + this.actors = []; + this.image = ''; + + this.released = false; + this.streamingServices = []; + this.airing = false; + this.airedFrom = ''; + this.airedTo = ''; + + this.userData = { + watched: false, + lastWatched: '', + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!obj.hasOwnProperty('userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'tv', 'season']; + } + + getMediaType(): MediaType { + return MediaType.Season; + } + + getSummary(): string { + return 'Season ' + this.seasonNumber + '(' + this.year + ')'; + } +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c7e5ac7..255f0b7 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -13,6 +13,7 @@ import { FolderSuggest } from './suggesters/FolderSuggest'; export interface MediaDbPluginSettings { OMDbKey: string; + TMDBKey: string; MobyGamesKey: string; GiantBombKey: string; ComicVineKey: string; @@ -23,6 +24,9 @@ export interface MediaDbPluginSettings { useDefaultFrontMatter: boolean; enableTemplaterIntegration: boolean; OMDbAPI_disabledMediaTypes: MediaType[]; + TMDBSeriesAPI_disabledMediaTypes: MediaType[]; + TMDBSeasonAPI_disabledMediaTypes: MediaType[]; + TMDBMovieAPI_disabledMediaTypes: MediaType[]; MALAPI_disabledMediaTypes: MediaType[]; MALAPIManga_disabledMediaTypes: MediaType[]; ComicVineAPI_disabledMediaTypes: MediaType[]; @@ -35,6 +39,7 @@ export interface MediaDbPluginSettings { OpenLibraryAPI_disabledMediaTypes: MediaType[]; movieTemplate: string; seriesTemplate: string; + seasonTemplate: string; mangaTemplate: string; gameTemplate: string; wikiTemplate: string; @@ -44,6 +49,7 @@ export interface MediaDbPluginSettings { movieFileNameTemplate: string; seriesFileNameTemplate: string; + seasonFileNameTemplate: string; mangaFileNameTemplate: string; gameFileNameTemplate: string; wikiFileNameTemplate: string; @@ -53,6 +59,7 @@ export interface MediaDbPluginSettings { moviePropertyConversionRules: string; seriesPropertyConversionRules: string; + seasonPropertyConversionRules: string; mangaPropertyConversionRules: string; gamePropertyConversionRules: string; wikiPropertyConversionRules: string; @@ -62,6 +69,7 @@ export interface MediaDbPluginSettings { movieFolder: string; seriesFolder: string; + seasonFolder: string; mangaFolder: string; gameFolder: string; wikiFolder: string; @@ -76,6 +84,7 @@ export interface MediaDbPluginSettings { const DEFAULT_SETTINGS: MediaDbPluginSettings = { OMDbKey: '', + TMDBKey: '', MobyGamesKey: '', GiantBombKey: '', ComicVineKey: '', @@ -86,6 +95,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { useDefaultFrontMatter: true, enableTemplaterIntegration: false, OMDbAPI_disabledMediaTypes: [], + TMDBSeriesAPI_disabledMediaTypes: [], + TMDBSeasonAPI_disabledMediaTypes: [], + TMDBMovieAPI_disabledMediaTypes: [], MALAPI_disabledMediaTypes: [], MALAPIManga_disabledMediaTypes: [], ComicVineAPI_disabledMediaTypes: [], @@ -98,6 +110,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { OpenLibraryAPI_disabledMediaTypes: [], movieTemplate: '', seriesTemplate: '', + seasonTemplate: '', mangaTemplate: '', gameTemplate: '', wikiTemplate: '', @@ -107,6 +120,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieFileNameTemplate: '{{ title }} ({{ year }})', seriesFileNameTemplate: '{{ title }} ({{ year }})', + seasonFileNameTemplate: '{{ title }} ({{ year }})', mangaFileNameTemplate: '{{ title }} ({{ year }})', gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', @@ -116,6 +130,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { moviePropertyConversionRules: '', seriesPropertyConversionRules: '', + seasonPropertyConversionRules: '', mangaPropertyConversionRules: '', gamePropertyConversionRules: '', wikiPropertyConversionRules: '', @@ -125,6 +140,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { movieFolder: 'Media DB/movies', seriesFolder: 'Media DB/series', + seasonFolder: 'Media DB/series', mangaFolder: 'Media DB/comics', gameFolder: 'Media DB/games', wikiFolder: 'Media DB/wiki', @@ -396,6 +412,19 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Season folder') + .setDesc('Where newly imported seasons should be placed.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.seasonFolder) + .setValue(this.plugin.settings.seriesFolder) + .onChange(data => { + this.plugin.settings.seasonFolder = data; + void this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Comic and manga folder') .setDesc('Where newly imported comics and manga should be placed.') @@ -503,6 +532,19 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Season template') + .setDesc('Template file to be used when creating a new note for a season.') + .addSearch(cb => { + new FileSuggest(this.app, cb.inputEl); + cb.setPlaceholder('Example: seasonTemplate.md') + .setValue(this.plugin.settings.seasonTemplate) + .onChange(data => { + this.plugin.settings.seasonTemplate = data; + void this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Manga and Comics template') .setDesc('Template file to be used when creating a new note for a manga or a comic.') @@ -609,6 +651,18 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName('Season file name template') + .setDesc('Template for the file name used when creating a new note for a season.') + .addText(cb => { + cb.setPlaceholder(`Example: ${DEFAULT_SETTINGS.seasonFileNameTemplate}`) + .setValue(this.plugin.settings.seasonFileNameTemplate) + .onChange(data => { + this.plugin.settings.seasonFileNameTemplate = data; + void this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName('Manga and comic file name template') .setDesc('Template for the file name used when creating a new note for a manga or comic.') diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index 02d1c2c..857050f 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -1,6 +1,7 @@ export enum MediaType { Movie = 'movie', Series = 'series', + Season = 'season', ComicManga = 'comicManga', Game = 'game', MusicRelease = 'musicRelease', diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 6f60c3b..b9e1419 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -8,6 +8,7 @@ import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MovieModel } from '../models/MovieModel'; import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { SeriesModel } from '../models/SeriesModel'; +import { SeasonModel } from '../models/SeasonModel'; import { WikiModel } from '../models/WikiModel'; import type { MediaDbPluginSettings } from '../settings/Settings'; import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; @@ -17,6 +18,7 @@ import { replaceTags } from './Utils'; export const MEDIA_TYPES: MediaType[] = [ MediaType.Movie, MediaType.Series, + MediaType.Season, MediaType.ComicManga, MediaType.Game, MediaType.Wiki, @@ -40,6 +42,7 @@ export class MediaTypeManager { this.mediaFileNameTemplateMap = new Map(); this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.Season, settings.seasonFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.ComicManga, settings.mangaFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Game, settings.gameFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Wiki, settings.wikiFileNameTemplate); @@ -50,6 +53,7 @@ export class MediaTypeManager { this.mediaTemplateMap = new Map(); this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate); this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate); + this.mediaTemplateMap.set(MediaType.Season, settings.seasonTemplate); this.mediaTemplateMap.set(MediaType.ComicManga, settings.mangaTemplate); this.mediaTemplateMap.set(MediaType.Game, settings.gameTemplate); this.mediaTemplateMap.set(MediaType.Wiki, settings.wikiTemplate); @@ -62,6 +66,7 @@ export class MediaTypeManager { this.mediaFolderMap = new Map(); this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder); this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder); + this.mediaFolderMap.set(MediaType.Season, settings.seasonFolder); this.mediaFolderMap.set(MediaType.ComicManga, settings.mangaFolder); this.mediaFolderMap.set(MediaType.Game, settings.gameFolder); this.mediaFolderMap.set(MediaType.Wiki, settings.wikiFolder); @@ -138,6 +143,8 @@ export class MediaTypeManager { return new MovieModel(obj); } else if (mediaType === MediaType.Series) { return new SeriesModel(obj); + } else if (mediaType === MediaType.Season) { + return new SeasonModel(obj); } else if (mediaType === MediaType.ComicManga) { return new ComicMangaModel(obj); } else if (mediaType === MediaType.Game) {