From 2a120ca5492614a3e7367990f3859cd8fb9422c3 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 00:49:38 -0700 Subject: [PATCH 01/60] Fix build errors --- automation/build/esbuild.config.ts | 4 +- package-lock.json | 5780 ++++++++++++++++++++++++++++ package.json | 1 - src/obsidian-setting-group.d.ts | 12 + styles.css | 21 + vite.config.ts => vite.config.mts | 9 +- 6 files changed, 5821 insertions(+), 6 deletions(-) create mode 100644 package-lock.json create mode 100644 src/obsidian-setting-group.d.ts create mode 100644 styles.css rename vite.config.ts => vite.config.mts (89%) diff --git a/automation/build/esbuild.config.ts b/automation/build/esbuild.config.ts index 20d35b8f..44bddce6 100644 --- a/automation/build/esbuild.config.ts +++ b/automation/build/esbuild.config.ts @@ -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'; @@ -26,7 +26,7 @@ const build = await esbuild.build({ '@lezer/common', '@lezer/highlight', '@lezer/lr', - ...builtins, + ...builtinModules, ], format: 'cjs', target: 'es2018', diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..79e04a64 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5780 @@ +{ + "name": "obsidian-media-db-plugin", + "version": "0.8.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "obsidian-media-db-plugin", + "version": "0.8.0", + "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", + "openapi-fetch": "^0.14.1", + "openapi-typescript": "^7.10.1", + "prettier": "^3.8.1", + "solid-js": "^1.9.3", + "string-argv": "^0.3.2", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^6.0.5", + "vite-plugin-banner": "^0.8.1", + "vite-plugin-solid": "^2.11.0", + "vite-plugin-static-copy": "^3.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.37.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", + "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@happy-dom/global-registrator": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@happy-dom/global-registrator/-/global-registrator-18.0.1.tgz", + "integrity": "sha512-xCy/cpEP8xyJ6u0eokYgaQxeUmcKqHx/+aC3R0DLa7/S38efhZAVDQqLJ5zzTguLFS0gvAzZHP40NGaLwRyapQ==", + "dev": true, + "dependencies": { + "@types/node": "^20.0.0", + "happy-dom": "^18.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lemons_dev/parsinom": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@lemons_dev/parsinom/-/parsinom-0.0.12.tgz", + "integrity": "sha512-i6oUfQfhw4ZStScMpPHy8ZmLrkn29RX/uK1SBKSKPuH0w9vOFQjZ0O4ev1hdk0K/eU196mk9mAlI1bjbO4n4sQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.2.tgz", + "integrity": "sha512-rcbDZOfXAgGEJeJ30aWCVVJvxV9ooevb/m1/SFblO2qHs4cqTk178gx7T/vdslf57EA4lTofrwsq5K8rxK9g+g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "dev": true + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", + "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", + "dev": true, + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bun": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", + "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", + "dev": true, + "dependencies": { + "bun-types": "1.3.11" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.6", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.6.tgz", + "integrity": "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", + "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", + "dev": true, + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.12" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bun-types": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", + "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.328", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", + "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "dev": true + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-only-warn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.1.0.tgz", + "integrity": "sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/happy-dom": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz", + "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", + "dev": true, + "dependencies": { + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iso-639-2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/iso-639-2/-/iso-639-2-3.0.2.tgz", + "integrity": "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obsidian": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", + "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/openapi-fetch": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.1.tgz", + "integrity": "sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==", + "dev": true, + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.1.tgz", + "integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==", + "dev": true, + "dependencies": { + "@redocly/openapi-core": "^1.34.5", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "dev": true + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.1.tgz", + "integrity": "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.1.tgz", + "integrity": "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/solid-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", + "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", + "dev": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/solid-refresh": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz", + "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.23.6", + "@babel/helper-module-imports": "^7.22.15", + "@babel/types": "^7.23.6" + }, + "peerDependencies": { + "solid-js": "^1.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-banner": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/vite-plugin-banner/-/vite-plugin-banner-0.8.1.tgz", + "integrity": "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ==", + "dev": true + }, + "node_modules/vite-plugin-solid": { + "version": "2.11.11", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.11.tgz", + "integrity": "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.3", + "@types/babel__core": "^7.20.4", + "babel-preset-solid": "^1.8.4", + "merge-anything": "^5.1.7", + "solid-refresh": "^0.6.3", + "vitefu": "^1.0.4" + }, + "peerDependencies": { + "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", + "solid-js": "^1.7.2", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/jest-dom": { + "optional": true + } + } + }, + "node_modules/vite-plugin-static-copy": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz", + "integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.6.0", + "p-map": "^7.0.4", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/sapphi-red" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dev": true, + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 92d08d1c..1a301c09 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@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", diff --git a/src/obsidian-setting-group.d.ts b/src/obsidian-setting-group.d.ts new file mode 100644 index 00000000..2018e48b --- /dev/null +++ b/src/obsidian-setting-group.d.ts @@ -0,0 +1,12 @@ +import type { Setting } from 'obsidian'; + +declare module 'obsidian' { + /** + * Settings section container (Obsidian 1.6+). Types lag the runtime API. + */ + export class SettingGroup { + constructor(containerEl: HTMLElement); + setHeading(name: string): this; + addSetting(cb: (setting: Setting) => unknown): void; + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 00000000..9b9260d6 --- /dev/null +++ b/styles.css @@ -0,0 +1,21 @@ +/* +------------------------------------------- +Media DB - Release Build +------------------------------------------- +By: Moritz Jung (https://www.moritzjung.dev) +Time: Sun, 29 Mar 2026 07:47:20 GMT +Version: 0.8.0 +------------------------------------------- +THIS IS A GENERATED/BUNDLED FILE +if you want to view the source, please visit the github repository of this plugin +------------------------------------------- +Copyright (c) 2026 Moritz Jung +------------------------------------------- +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. If not, see . +*/ + +.media-db-plugin-list-wrapper{display:flex;align-content:center;margin-bottom:5px;margin-top:5px}.media-db-plugin-list-text-wrapper{flex:1}.media-db-plugin-list-text{display:block}small.media-db-plugin-list-text{color:var(--text-muted)}.media-db-plugin-select-modal{display:contents}.media-db-plugin-select-wrapper{display:flex;flex-direction:column;margin:5px;overflow-y:auto}.media-db-plugin-select-element{cursor:pointer;border-left:5px solid transparent;padding:5px;margin:5px 0;border-radius:5px;white-space:pre-wrap;font-size:16px}.media-db-plugin-select-element-selected{border-left:5px solid var(--interactive-accent)!important;background:var(--background-secondary-alt)}.media-db-plugin-select-element-hover{background:var(--background-secondary-alt)}.media-db-plugin-preview-modal{display:contents}.media-db-plugin-preview-wrapper{display:flex;flex-direction:column;overflow-y:auto}.media-db-plugin-spacer{margin-bottom:10px}.media-db-plugin-preview{border-radius:var(--modal-radius);border:var(--modal-border-width) solid var(--modal-border-color);padding:var(--size-4-4)}.icon-wrapper{display:inline-block;position:relative;width:20px}.icon{position:absolute;height:20px;width:20px;top:calc(50% - 10px)}.media-db-plugin-property-mappings-model-container{margin-bottom:var(--size-4-8)}.media-db-plugin-property-mappings-model-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--size-4-4);gap:var(--size-4-3)}.media-db-plugin-property-mappings-model-header .setting-item-name{font-weight:var(--font-semibold);font-size:var(--font-ui-medium);color:var(--text-normal);margin:0}.media-db-plugin-property-mappings-model-actions{display:flex;align-items:center;gap:var(--size-4-3)}.media-db-plugin-property-mapping-unsaved-changes{color:var(--text-warning);font-size:var(--font-ui-small);white-space:nowrap}.media-db-plugin-property-mappings-save-button{white-space:nowrap;cursor:pointer}.media-db-plugin-property-mappings-save-button.mod-muted{opacity:.5;cursor:not-allowed}.media-db-plugin-property-mapping-validation{color:var(--text-error);background:rgba(var(--color-red-rgb),.1);padding:var(--size-4-3) var(--size-4-4);margin-bottom:var(--size-4-4);border-left:3px solid var(--text-error);font-size:var(--font-ui-small);line-height:1.5;border-radius:var(--radius-s)}.media-db-plugin-property-mappings-table-container{overflow-x:auto}.media-db-plugin-property-mappings-table{width:100%;border-collapse:collapse;border-spacing:0;font-size:var(--font-ui-small)}.media-db-plugin-property-mappings-table thead{border-bottom:1px solid var(--background-modifier-border)}.media-db-plugin-property-mappings-table th{padding:var(--size-4-2) var(--size-4-3);padding-left:0;text-align:left;font-weight:var(--font-semibold);color:var(--text-muted);font-size:var(--font-ui-smaller);text-transform:uppercase;letter-spacing:.02em;border-bottom:none}.media-db-plugin-property-mappings-table tbody tr{transition:background-color .1s ease}.media-db-plugin-property-mappings-table td{padding:var(--size-4-3) var(--size-4-3) var(--size-4-3) 0;border-bottom:1px solid var(--background-modifier-border-hover);vertical-align:middle}.media-db-plugin-property-mappings-table tbody tr:last-child td{border-bottom:none}.col-property{width:25%;white-space:nowrap}.col-mapping{width:20%}.col-new-name{width:40%}.col-wikilink{width:15%;text-align:center}.col-locked{text-align:center;font-style:italic}.media-db-plugin-property-mappings-table code{padding:var(--size-4-1) var(--size-4-2);margin:0;background:var(--code-background);color:var(--code-normal);border-radius:var(--radius-s);font-size:var(--font-ui-smaller);font-family:var(--font-monospace)}.media-db-plugin-property-binding-text{color:var(--text-muted);font-size:var(--font-ui-small);font-style:italic}.media-db-plugin-property-mappings-table select.dropdown{width:100%;max-width:100%}.media-db-plugin-property-mapping-to{display:flex;align-items:center;gap:var(--size-4-2);min-width:0}.media-db-plugin-property-mapping-input{flex:1;width:100%;font-family:var(--font-monospace)}.media-db-plugin-property-mapping-to-disabled{color:var(--text-faint);font-size:var(--font-ui-medium)}.media-db-plugin-property-mapping-wikilink-label{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:var(--size-4-1)}.media-db-plugin-property-mapping-wikilink-label input[type=checkbox]{cursor:pointer;width:var(--checkbox-size);height:var(--checkbox-size)} diff --git a/vite.config.ts b/vite.config.mts similarity index 89% rename from vite.config.ts rename to vite.config.mts index ffa6b85d..d833a09a 100644 --- a/vite.config.ts +++ b/vite.config.mts @@ -1,11 +1,14 @@ +import { builtinModules } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; -import builtins from 'builtin-modules'; import { getBuildBanner } from './automation/build/buildBanner'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import banner from 'vite-plugin-banner'; import manifest from './manifest.json' with { type: 'json' }; -import path from 'path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const entryFile = 'src/main.ts'; @@ -72,7 +75,7 @@ export default defineConfig(({ mode }) => { '@lezer/common', '@lezer/highlight', '@lezer/lr', - ...builtins, + ...builtinModules, ], }, }, From cb5e77af3bdc2b80f6f82e7580a90b34eae9b71e Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:21:13 -0700 Subject: [PATCH 02/60] Added tabs for settings --- src/settings/Settings.ts | 184 ++++++++++++++++++++++++++------------- src/styles.css | 100 +++++++++++++++++++++ 2 files changed, 223 insertions(+), 61 deletions(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 9566bfc0..ff913802 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,5 +1,5 @@ -import type { App } from 'obsidian'; -import { Notice, PluginSettingTab, SettingGroup } from 'obsidian'; +import type { App, IconName } from 'obsidian'; +import { Notice, Platform, PluginSettingTab, SettingGroup, setIcon } from 'obsidian'; import { render } from 'solid-js/web'; import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; @@ -12,6 +12,29 @@ import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; import { FileSuggest } from './suggesters/FileSuggest'; import { FolderSuggest } from './suggesters/FolderSuggest'; +function mediaTypeTabIcon(mediaType: MediaType): IconName { + switch (mediaType) { + case MediaType.Movie: + return 'film'; + case MediaType.Series: + return 'tv'; + case MediaType.Season: + return 'calendar-range'; + case MediaType.ComicManga: + return 'book-open'; + case MediaType.Game: + return 'gamepad-2'; + case MediaType.Wiki: + return 'library-big'; + case MediaType.MusicRelease: + return 'disc-3'; + case MediaType.BoardGame: + return 'dice-3'; + case MediaType.Book: + return 'book-marked'; + } +} + // MARK: Settings export interface MediaDbPluginSettings { OMDbKey: string; @@ -385,9 +408,16 @@ export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings return defaultSettings; } +interface MediaDbSettingsTabNavEntry { + id: string; + nav: HTMLElement; + panel: HTMLElement; +} + // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; + private activeSettingsTabId: string | null = null; constructor(app: App, plugin: MediaDbPlugin) { super(app, plugin); @@ -398,12 +428,48 @@ export class MediaDbSettingTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); + const headerNav = containerEl.createEl('nav', { cls: 'media-db-setting-header' }); + const tabGroup = headerNav.createDiv({ cls: 'media-db-setting-tab-group' }); + const settingsContentEl = containerEl.createDiv({ cls: 'media-db-setting-content' }); + + const tabEntries: MediaDbSettingsTabNavEntry[] = []; + + const selectTab = (id: string): void => { + for (const { id: tid, nav, panel } of tabEntries) { + const on = tid === id; + panel.toggleClass('media-db-tab-settings--hidden', !on); + nav.toggleClass('media-db-navigation-item-selected', on); + } + this.activeSettingsTabId = id; + }; + + const addTab = (id: string, title: string, icon: IconName, render: (panel: HTMLElement) => void): void => { + const nav = tabGroup.createDiv({ cls: 'media-db-navigation-item' }); + nav.addClass(Platform.isMobile ? 'media-db-mobile' : 'media-db-desktop'); + setIcon(nav.createSpan({ cls: 'media-db-navigation-item-icon' }), icon); + nav.createSpan().setText(title); + const panel = settingsContentEl.createDiv({ cls: 'media-db-tab-settings media-db-tab-settings--hidden' }); + render(panel); + tabEntries.push({ id, nav, panel }); + nav.addEventListener('click', () => selectTab(id)); + }; + const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); - // MARK: General settings - const generalGroup = new SettingGroup(containerEl); + const mediaTypeApiMap = new Map(); + for (const api of this.plugin.apiManager.apis) { + for (const mediaType of api.types) { + if (!mediaTypeApiMap.has(mediaType)) { + mediaTypeApiMap.set(mediaType, []); + } + mediaTypeApiMap.get(mediaType)!.push(api.apiName); + } + } + + addTab('general', 'General', 'sliders-horizontal', panel => { + const generalGroup = new SettingGroup(panel); - generalGroup.addSetting( + generalGroup.addSetting( setting => void setting .setName('SFW filter') @@ -534,16 +600,16 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }), ); + }); - // MARK: API keys - const apiKeyGroup = new SettingGroup(containerEl); - apiKeyGroup.setHeading('API Keys'); + addTab('api-keys', 'API keys', 'key', panel => { + const apiKeyGroup = new SettingGroup(panel); - apiKeyGroup.addSetting( - setting => - void setting - .setName('OMDb API key') - .setDesc('API key for "www.omdbapi.com".') + apiKeyGroup.addSetting( + setting => + void setting + .setName('OMDb API key') + .setDesc('API key for "www.omdbapi.com".') // .addComponent((el) => { // let component = new SecretComponent(this.app, el); @@ -675,31 +741,17 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }), ); - - // MARK: Media type settings - - // Create a map to store APIs for each media type - const mediaTypeApiMap = new Map(); - - // Populate the map with APIs for each media type dynamically - for (const api of this.plugin.apiManager.apis) { - for (const mediaType of api.types) { - if (!mediaTypeApiMap.has(mediaType)) { - mediaTypeApiMap.set(mediaType, []); - } - mediaTypeApiMap.get(mediaType)!.push(api.apiName); - } - } + }); for (const mediaTypeSetting of mediaTypeSettings) { - const mediaTypeGroup = new SettingGroup(containerEl); const mediaType = mediaTypeSetting.mediaType; const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); - const mediaTypeNameLower = mediaTypeName.toLowerCase(); - mediaTypeGroup.setHeading(`${mediaTypeName} settings`); + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + const mediaTypeGroup = new SettingGroup(panel); + const mediaTypeNameLower = mediaTypeName.toLowerCase(); - // Folder + // Folder mediaTypeGroup.addSetting( setting => void setting @@ -791,42 +843,52 @@ export class MediaDbSettingTab extends PluginSettingTab { } } } + }); } // MARK: Property mappings if (this.plugin.settings.useDefaultFrontMatter) { - const mappingGroup = new SettingGroup(containerEl); - mappingGroup.setHeading('Property mappings'); - mappingGroup.addSetting(setting => { - setting - .setName('Property mappings explanation') - .setDesc( - fragWithHTML( - '

Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.

' + - '

You can choose to keep the original name, rename the property, or remove it entirely.

' + - '

Remember to save your changes using the save button for each individual category.

', - ), - ); - - render( - () => - PropertyMappingModelsComponent({ - models: structuredClone(this.plugin.settings.propertyMappingModels), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } + addTab('property-mappings', 'Property mappings', 'list-tree', panel => { + const mappingGroup = new SettingGroup(panel); + mappingGroup.addSetting(setting => { + setting + .setName('Property mappings explanation') + .setDesc( + fragWithHTML( + '

Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.

' + + '

You can choose to keep the original name, rename the property, or remove it entirely.

' + + '

Remember to save your changes using the save button for each individual category.

', + ), + ); - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); - void this.plugin.saveSettings(); - }, - }), - setting.descEl, - ); + render( + () => + PropertyMappingModelsComponent({ + models: structuredClone(this.plugin.settings.propertyMappingModels), + save: (model: PropertyMappingModelData): void => { + // Update the matching model in settings (stored as plain data) + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + + new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + void this.plugin.saveSettings(); + }, + }), + setting.descEl, + ); + }); }); } + + const validIds = new Set(tabEntries.map(t => t.id)); + let initialId = + this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; + if (!validIds.has(initialId)) { + initialId = 'general'; + } + selectTab(initialId); } } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index b204d1d6..f605a85c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -257,3 +257,103 @@ small.media-db-plugin-list-text { width: var(--checkbox-size); height: var(--checkbox-size); } + +/* + * Settings tabs: same layout pattern as Obsidian Linter (horizontal icon tabs). + * Adapted from https://github.com/platers/obsidian-linter/blob/master/src/styles.css (MIT). + */ +.media-db-navigation-item { + cursor: pointer; + border-radius: 8px 8px 2px 2px; + border: 1px solid var(--background-modifier-border); + font-weight: bold; + font-size: 16px; + display: flex; + flex-direction: row; + white-space: nowrap; + padding: 4px 6px; + align-items: center; + gap: 4px; + overflow: hidden; + background-color: var(--background-primary-secondary-alt); + transition: + color 0.25s ease-in-out, + padding 0.25s ease-in-out, + background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), + max-width 0.35s cubic-bezier(0.57, 0.04, 0.58, 1); + height: 32px; +} + +@media screen and (max-width: 1325px) { + .media-db-navigation-item.media-db-desktop { + max-width: 32px; + } +} + +@media screen and (max-width: 800px) { + .media-db-navigation-item.media-db-mobile { + max-width: 32px; + } +} + +.media-db-navigation-item-icon { + padding-top: 5px; +} + +.media-db-navigation-item:hover { + border-color: var(--interactive-accent-hover); + border-bottom: 0; +} + +.media-db-navigation-item-selected { + background-color: var(--interactive-accent) !important; + color: var(--text-on-accent); + padding: 4px 9px !important; + max-width: 100% !important; + border: 1px solid var(--background-modifier-border); + border-radius: 8px 8px 2px 2px; + border-bottom: 0; + transition: + color 0.25s ease-in-out, + padding 0.25s ease-in-out, + background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), + max-width 0.45s cubic-bezier(0.57, 0.04, 0.58, 1) 0.2s; +} + +.media-db-setting-header { + margin-bottom: 24px; + overflow-y: hidden; + overflow-x: auto; +} + +.media-db-setting-header .media-db-setting-tab-group { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + width: 100%; +} + +.media-db-setting-tab-group { + margin-top: 6px; + padding-left: 2px; + padding-right: 2px; + border-bottom: 2px solid var(--background-modifier-border); +} + +.media-db-tab-settings--hidden { + display: none !important; +} + +.media-db-navigation-item:not(.media-db-navigation-item-selected) > span:nth-child(2), +.media-db-visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} From 4bb16628cf5a9301ca7bfb7be3a0e869c0d69767 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:26:30 -0700 Subject: [PATCH 03/60] Refactor API keys to use Obsidian keychain manager --- manifest-beta.json | 2 +- manifest.json | 2 +- src/api/apis/BoardGameGeekAPI.ts | 15 ++- src/api/apis/ComicVineAPI.ts | 15 ++- src/api/apis/GiantBombAPI.ts | 9 +- src/api/apis/IGDBAPI.ts | 13 +- src/api/apis/MobyGamesAPI.ts | 9 +- src/api/apis/OMDbAPI.ts | 11 +- src/api/apis/RAWGAPI.ts | 9 +- src/api/apis/TMDBMovieAPI.ts | 9 +- src/api/apis/TMDBSeasonAPI.ts | 17 +-- src/api/apis/TMDBSeriesAPI.ts | 9 +- src/main.ts | 5 +- src/obsidian-secrets-augment.d.ts | 25 ++++ src/settings/Settings.ts | 189 ++++++++---------------------- src/settings/apiSecretHelpers.ts | 67 +++++++++++ src/settings/apiSecretIds.ts | 17 +++ 17 files changed, 243 insertions(+), 180 deletions(-) create mode 100644 src/obsidian-secrets-augment.d.ts create mode 100644 src/settings/apiSecretHelpers.ts create mode 100644 src/settings/apiSecretIds.ts diff --git a/manifest-beta.json b/manifest-beta.json index e93a0224..fc51c90d 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -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", diff --git a/manifest.json b/manifest.json index a8d101ff..56b232b4 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 07c20982..31973c42 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -1,6 +1,7 @@ import { requestUrl } from 'obsidian'; import { BoardGameModel } from 'src/models/BoardGameModel'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -23,11 +24,16 @@ export class BoardGameGeekAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); + const bggKey = apiSecrets.boardgameGeek(this.plugin); + if (!bggKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; const fetchData = await requestUrl({ url: searchUrl, headers: { - Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`, + Authorization: `Bearer ${bggKey}`, }, }); @@ -68,11 +74,16 @@ export class BoardGameGeekAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); + const bggKey = apiSecrets.boardgameGeek(this.plugin); + if (!bggKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; const fetchData = await requestUrl({ url: searchUrl, headers: { - Authorization: `Bearer ${this.plugin.settings.BoardgameGeekKey}`, + Authorization: `Bearer ${bggKey}`, }, }); diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index 53d98207..555b122e 100644 --- a/src/api/apis/ComicVineAPI.ts +++ b/src/api/apis/ComicVineAPI.ts @@ -3,6 +3,7 @@ import { requestUrl } from 'obsidian'; import { ComicMangaModel } from 'src/models/ComicMangaModel'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -25,7 +26,12 @@ export class ComicVineAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const searchUrl = `${this.apiUrl}/search/?api_key=${this.plugin.settings.ComicVineKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const apiKey = apiSecrets.comicVine(this.plugin); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/search/?api_key=${apiKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -56,7 +62,12 @@ export class ComicVineAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${this.plugin.settings.ComicVineKey}&format=json`; + const apiKey = apiSecrets.comicVine(this.plugin); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${apiKey}&format=json`; const fetchData = await requestUrl({ url: searchUrl, }); diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 76e451b2..26a4a36e 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -1,6 +1,7 @@ import createClient from 'openapi-fetch'; import { obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; @@ -24,7 +25,7 @@ export class GiantBombAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.GiantBombKey) { + if (!apiSecrets.giantBomb(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -32,7 +33,7 @@ export class GiantBombAPI extends APIModel { const response = await client.GET('/games', { params: { query: { - api_key: this.plugin.settings.GiantBombKey, + api_key: apiSecrets.giantBomb(this.plugin), filter: `name:${title}`, format: 'json', limit: 20, @@ -74,7 +75,7 @@ export class GiantBombAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.GiantBombKey) { + if (!apiSecrets.giantBomb(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -85,7 +86,7 @@ export class GiantBombAPI extends APIModel { guid: id, }, query: { - api_key: this.plugin.settings.GiantBombKey, + api_key: apiSecrets.giantBomb(this.plugin), format: 'json', }, }, diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 57a42f1e..8b717e36 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -2,6 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -35,12 +36,14 @@ export class IGDBAPI extends APIModel { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - if (!this.plugin.settings.IGDBClientId || !this.plugin.settings.IGDBClientSecret) { + const clientId = apiSecrets.igdbClientId(this.plugin); + const clientSecret = apiSecrets.igdbClientSecret(this.plugin); + if (!clientId || !clientSecret) { throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); } console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${this.plugin.settings.IGDBClientId}&client_secret=${this.plugin.settings.IGDBClientSecret}&grant_type=client_credentials`, + url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, method: 'POST', }); if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); @@ -53,10 +56,11 @@ export class IGDBAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); const token = await this.getAuthToken(); + const clientId = apiSecrets.igdbClientId(this.plugin); const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); @@ -75,10 +79,11 @@ export class IGDBAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); const token = await this.getAuthToken(); + const clientId = apiSecrets.igdbClientId(this.plugin); const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': this.plugin.settings.IGDBClientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index a88eec94..0b8a6086 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -2,6 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; @@ -28,11 +29,11 @@ export class MobyGamesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.MobyGamesKey) { + if (!apiSecrets.mobyGames(this.plugin)) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${this.plugin.settings.MobyGamesKey}`; + const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${apiSecrets.mobyGames(this.plugin)}`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -71,11 +72,11 @@ export class MobyGamesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.MobyGamesKey) { + if (!apiSecrets.mobyGames(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${this.plugin.settings.MobyGamesKey}`; + const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${apiSecrets.mobyGames(this.plugin)}`; const fetchData = await requestUrl({ url: searchUrl, }); diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 51b4f17a..362fb7ad 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -4,6 +4,7 @@ import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import { SeriesModel } from '../../models/SeriesModel'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -77,12 +78,13 @@ export class OMDbAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.OMDbKey) { + const omdbKey = apiSecrets.omdb(this.plugin); + if (!omdbKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const response = await requestUrl({ - url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${this.plugin.settings.OMDbKey}`, + url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${omdbKey}`, method: 'GET', }); @@ -161,12 +163,13 @@ export class OMDbAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.OMDbKey) { + const omdbKey = apiSecrets.omdb(this.plugin); + if (!omdbKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const response = await requestUrl({ - url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${this.plugin.settings.OMDbKey}`, + url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${omdbKey}`, method: 'GET', }); diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index 1c79e5ae..8143fc09 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -2,6 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -26,9 +27,9 @@ export class RAWGAPI extends APIModel { } async searchByTitle(title: string): Promise { - if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + if (!apiSecrets.rawg(this.plugin)) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${this.plugin.settings.RAWGAPIKey}&search=${encodeURIComponent(title)}&page_size=20`, + url: `${this.apiUrl}/games?key=${apiSecrets.rawg(this.plugin)}&search=${encodeURIComponent(title)}&page_size=20`, method: 'GET', }); if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); @@ -42,9 +43,9 @@ export class RAWGAPI extends APIModel { } async getById(id: string): Promise { - if (!this.plugin.settings.RAWGAPIKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + if (!apiSecrets.rawg(this.plugin)) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${this.plugin.settings.RAWGAPIKey}`, + url: `${this.apiUrl}/games/${id}?key=${apiSecrets.rawg(this.plugin)}`, method: 'GET', }); if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 936ab57e..66676078 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -2,6 +2,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import { MediaType } from '../../utils/MediaType'; @@ -28,14 +29,14 @@ export class TMDBMovieAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/search/movie', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { query: { @@ -86,14 +87,14 @@ export class TMDBMovieAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/movie/{movie_id}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { movie_id: parseInt(id) }, diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 426f6db5..2fe25232 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -2,6 +2,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { SeasonModel } from '../../models/SeasonModel'; import { MediaType } from '../../utils/MediaType'; @@ -28,14 +29,14 @@ export class TMDBSeasonAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const searchResponse = await client.GET('/3/search/tv', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { query: { @@ -70,7 +71,7 @@ export class TMDBSeasonAPI extends APIModel { try { const detailsResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { series_id: result.id ?? 0 }, @@ -106,14 +107,14 @@ export class TMDBSeasonAPI extends APIModel { // Fetch all seasons for a given series async getSeasonsForSeries(tvId: string): Promise { - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const seriesResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { series_id: parseInt(tvId) }, @@ -159,7 +160,7 @@ export class TMDBSeasonAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -177,7 +178,7 @@ export class TMDBSeasonAPI extends APIModel { // Fetch season details const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { @@ -203,7 +204,7 @@ export class TMDBSeasonAPI extends APIModel { // Fetch parent series to build consistent titles and inherit fields const seriesResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { series_id: parseInt(tvId) }, diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 1c8a21fa..96b8d9ee 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -2,6 +2,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; +import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { SeriesModel } from '../../models/SeriesModel'; import { MediaType } from '../../utils/MediaType'; @@ -28,14 +29,14 @@ export class TMDBSeriesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/search/tv', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { query: { @@ -86,14 +87,14 @@ export class TMDBSeriesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!this.plugin.settings.TMDBKey) { + if (!apiSecrets.tmdb(this.plugin)) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${this.plugin.settings.TMDBKey}`, + Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, }, params: { path: { series_id: parseInt(id) }, diff --git a/src/main.ts b/src/main.ts index 7f7019ed..4762664f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,13 +7,13 @@ import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { ComicVineAPI } from './api/apis/ComicVineAPI'; import { GiantBombAPI } from './api/apis/GiantBombAPI'; import { IGDBAPI } from './api/apis/IGDBAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; +import { RAWGAPI } from './api/apis/RAWGAPI'; import { SteamAPI } from './api/apis/SteamAPI'; import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; @@ -25,6 +25,7 @@ import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import type { MediaTypeModel } from './models/MediaTypeModel'; import type { SeasonModel } from './models/SeasonModel'; +import { migrateLegacyApiKeysToSecretStorage, stripPlaintextApiKeysFromSettings } from './settings/apiSecretHelpers'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; @@ -723,9 +724,11 @@ export default class MediaDbPlugin extends Plugin { loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); this.settings = loadedSettings; + migrateLegacyApiKeysToSecretStorage(this); } async saveSettings(): Promise { + stripPlaintextApiKeysFromSettings(this.settings); this.mediaTypeManager.updateTemplates(this.settings); this.mediaTypeManager.updateFolders(this.settings); this.dateFormatter.setFormat(this.settings.customDateFormat); diff --git a/src/obsidian-secrets-augment.d.ts b/src/obsidian-secrets-augment.d.ts new file mode 100644 index 00000000..2d4cdc65 --- /dev/null +++ b/src/obsidian-secrets-augment.d.ts @@ -0,0 +1,25 @@ +import type { App, BaseComponent } from 'obsidian'; + +declare module 'obsidian' { + /** @public @since 1.11.4 */ + export class SecretStorage { + setSecret(id: string, secret: string): void; + getSecret(id: string): string | null; + listSecrets(): string[]; + } + + /** @public @since 1.11.1 */ + export class SecretComponent extends BaseComponent { + constructor(app: App, containerEl: HTMLElement); + setValue(value: string): this; + onChange(cb: (value: string) => unknown): this; + } + + interface App { + secretStorage: SecretStorage; + } + + interface Setting { + addComponent(cb: (el: HTMLElement) => T): this; + } +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index ff913802..d95f7606 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,11 +1,13 @@ import type { App, IconName } from 'obsidian'; -import { Notice, Platform, PluginSettingTab, SettingGroup, setIcon } from 'obsidian'; +import { Notice, Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; import { render } from 'solid-js/web'; import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { fragWithHTML, unCamelCase } from '../utils/Utils'; +import type { ApiSecretId } from './apiSecretIds'; +import { API_SECRET_IDS } from './apiSecretIds'; import type { PropertyMappingModelData } from './PropertyMapping'; import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; @@ -424,6 +426,21 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin = plugin; } + private addApiSecretSetting(group: SettingGroup, name: string, description: string, secretId: ApiSecretId): void { + group.addSetting(setting => + void setting.setName(name).setDesc(description).addComponent(el => { + const component = new SecretComponent(this.app, el); + component + .setValue(this.app.secretStorage.getSecret(secretId) ?? '') + .onChange(value => { + this.app.secretStorage.setSecret(secretId, value); + void this.plugin.saveSettings(); + }); + return component; + }), + ); + } + display(): void { const { containerEl } = this; containerEl.empty(); @@ -605,142 +622,40 @@ export class MediaDbSettingTab extends PluginSettingTab { addTab('api-keys', 'API keys', 'key', panel => { const apiKeyGroup = new SettingGroup(panel); - apiKeyGroup.addSetting( - setting => - void setting - .setName('OMDb API key') - .setDesc('API key for "www.omdbapi.com".') - // .addComponent((el) => { - // let component = new SecretComponent(this.app, el); - - // component.setValue(this.plugin.settings.OMDbKey).onChange(data => { - // this.plugin.settings.OMDbKey = data; - // void this.plugin.saveSettings(); - // }); - - // return component; - // }) - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.OMDbKey) - .onChange(data => { - this.plugin.settings.OMDbKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('TMDB API Token') - .setDesc('API Read Access Token for "https://www.themoviedb.org".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.TMDBKey) - .onChange(data => { - this.plugin.settings.TMDBKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('Moby Games key') - .setDesc('API key for "www.mobygames.com".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.MobyGamesKey) - .onChange(data => { - this.plugin.settings.MobyGamesKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('Giant Bomb Key') - .setDesc('API key for "www.giantbomb.com".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.GiantBombKey) - .onChange(data => { - this.plugin.settings.GiantBombKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('IGDB Client ID') - .setDesc('Client ID for IGDB API (Required for Twitch OAuth).') - .addText(cb => { - cb.setPlaceholder('Client ID') - .setValue(this.plugin.settings.IGDBClientId) - .onChange(data => { - this.plugin.settings.IGDBClientId = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('IGDB Client Secret') - .setDesc('Client Secret for IGDB API.') - .addText(cb => { - cb.setPlaceholder('Client Secret') - .setValue(this.plugin.settings.IGDBClientSecret) - .onChange(data => { - this.plugin.settings.IGDBClientSecret = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('RAWG API Key') - .setDesc('API key for "rawg.io".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.RAWGAPIKey) - .onChange(data => { - this.plugin.settings.RAWGAPIKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('Comic Vine Key') - .setDesc('API key for "www.comicvine.gamespot.com".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.ComicVineKey) - .onChange(data => { - this.plugin.settings.ComicVineKey = data; - void this.plugin.saveSettings(); - }); - }), - ); - apiKeyGroup.addSetting( - setting => - void setting - .setName('Boardgame Geek Key') - .setDesc('API key for "www.boardgamegeek.com".') - .addText(cb => { - cb.setPlaceholder('API key') - .setValue(this.plugin.settings.BoardgameGeekKey) - .onChange(data => { - this.plugin.settings.BoardgameGeekKey = data; - void this.plugin.saveSettings(); - }); - }), - ); + this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', API_SECRET_IDS.omdb); + this.addApiSecretSetting( + apiKeyGroup, + 'TMDB API Token', + 'API Read Access Token for "https://www.themoviedb.org".', + API_SECRET_IDS.tmdb, + ); + this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', API_SECRET_IDS.mobyGames); + this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', API_SECRET_IDS.giantBomb); + this.addApiSecretSetting( + apiKeyGroup, + 'IGDB Client ID', + 'Client ID for IGDB API (Required for Twitch OAuth).', + API_SECRET_IDS.igdbClientId, + ); + this.addApiSecretSetting( + apiKeyGroup, + 'IGDB Client Secret', + 'Client Secret for IGDB API.', + API_SECRET_IDS.igdbClientSecret, + ); + this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', API_SECRET_IDS.rawg); + this.addApiSecretSetting( + apiKeyGroup, + 'Comic Vine Key', + 'API key for "www.comicvine.gamespot.com".', + API_SECRET_IDS.comicVine, + ); + this.addApiSecretSetting( + apiKeyGroup, + 'Boardgame Geek Key', + 'API key for "www.boardgamegeek.com".', + API_SECRET_IDS.boardgameGeek, + ); }); for (const mediaTypeSetting of mediaTypeSettings) { diff --git a/src/settings/apiSecretHelpers.ts b/src/settings/apiSecretHelpers.ts new file mode 100644 index 00000000..dab3f0d9 --- /dev/null +++ b/src/settings/apiSecretHelpers.ts @@ -0,0 +1,67 @@ +import type MediaDbPlugin from 'src/main'; +import type { ApiSecretId } from './apiSecretIds'; +import { API_SECRET_IDS } from './apiSecretIds'; +import type { MediaDbPluginSettings } from './Settings'; + +const LEGACY_KEY_PAIRS: { id: ApiSecretId; legacy: keyof MediaDbPluginSettings }[] = [ + { id: API_SECRET_IDS.omdb, legacy: 'OMDbKey' }, + { id: API_SECRET_IDS.tmdb, legacy: 'TMDBKey' }, + { id: API_SECRET_IDS.mobyGames, legacy: 'MobyGamesKey' }, + { id: API_SECRET_IDS.giantBomb, legacy: 'GiantBombKey' }, + { id: API_SECRET_IDS.igdbClientId, legacy: 'IGDBClientId' }, + { id: API_SECRET_IDS.igdbClientSecret, legacy: 'IGDBClientSecret' }, + { id: API_SECRET_IDS.rawg, legacy: 'RAWGAPIKey' }, + { id: API_SECRET_IDS.comicVine, legacy: 'ComicVineKey' }, + { id: API_SECRET_IDS.boardgameGeek, legacy: 'BoardgameGeekKey' }, +]; + +/** Move plaintext API keys from data.json into Obsidian SecretStorage and clear legacy fields. */ +export function migrateLegacyApiKeysToSecretStorage(plugin: MediaDbPlugin): void { + const storage = plugin.app.secretStorage; + const { settings } = plugin; + let dirty = false; + + for (const { id, legacy } of LEGACY_KEY_PAIRS) { + const plaintext = settings[legacy]; + if (typeof plaintext !== 'string' || plaintext === '') { + continue; + } + if (!storage.getSecret(id)) { + storage.setSecret(id, plaintext); + } + dirty = true; + } + if (dirty) { + stripPlaintextApiKeysFromSettings(settings); + void plugin.saveSettings(); + } +} + +/** Prevent API keys from being written to data.json (defense in depth). */ +export function stripPlaintextApiKeysFromSettings(settings: MediaDbPluginSettings): void { + settings.OMDbKey = ''; + settings.TMDBKey = ''; + settings.MobyGamesKey = ''; + settings.GiantBombKey = ''; + settings.IGDBClientId = ''; + settings.IGDBClientSecret = ''; + settings.RAWGAPIKey = ''; + settings.ComicVineKey = ''; + settings.BoardgameGeekKey = ''; +} + +function getSecret(plugin: MediaDbPlugin, id: ApiSecretId): string { + return plugin.app.secretStorage.getSecret(id) ?? ''; +} + +export const apiSecrets = { + omdb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.omdb), + tmdb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.tmdb), + mobyGames: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.mobyGames), + giantBomb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.giantBomb), + igdbClientId: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.igdbClientId), + igdbClientSecret: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.igdbClientSecret), + rawg: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.rawg), + comicVine: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.comicVine), + boardgameGeek: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.boardgameGeek), +}; diff --git a/src/settings/apiSecretIds.ts b/src/settings/apiSecretIds.ts new file mode 100644 index 00000000..d88ac8ba --- /dev/null +++ b/src/settings/apiSecretIds.ts @@ -0,0 +1,17 @@ +/** + * Obsidian SecretStorage IDs (lowercase alphanumeric + dashes only). + * @see https://docs.obsidian.md/plugins/guides/secret-storage + */ +export const API_SECRET_IDS = { + omdb: 'media-db-omdb', + tmdb: 'media-db-tmdb', + mobyGames: 'media-db-moby-games', + giantBomb: 'media-db-giant-bomb', + igdbClientId: 'media-db-igdb-client-id', + igdbClientSecret: 'media-db-igdb-client-secret', + rawg: 'media-db-rawg', + comicVine: 'media-db-comic-vine', + boardgameGeek: 'media-db-boardgame-geek', +} as const; + +export type ApiSecretId = (typeof API_SECRET_IDS)[keyof typeof API_SECRET_IDS]; From e5efccf86de4e4a2c6b6ca09984b84333bd115c4 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:47:41 -0700 Subject: [PATCH 04/60] Added Band and Album media types --- src/api/GeniusClient.ts | 110 +++++ src/api/apis/MusicBrainzBandAPI.ts | 235 +++++++++ src/main.ts | 136 +++++- src/modals/MediaDbSearchModal.ts | 6 +- src/models/BandModel.ts | 60 +++ src/models/SongModel.ts | 80 +++ src/settings/Settings.ts | 747 ++++++++++++++++------------- src/settings/apiSecretIds.ts | 1 + src/utils/MediaType.ts | 12 +- src/utils/MediaTypeManager.ts | 14 + src/utils/Utils.ts | 9 + 11 files changed, 1060 insertions(+), 350 deletions(-) create mode 100644 src/api/GeniusClient.ts create mode 100644 src/api/apis/MusicBrainzBandAPI.ts create mode 100644 src/models/BandModel.ts create mode 100644 src/models/SongModel.ts diff --git a/src/api/GeniusClient.ts b/src/api/GeniusClient.ts new file mode 100644 index 00000000..4f9bf6b9 --- /dev/null +++ b/src/api/GeniusClient.ts @@ -0,0 +1,110 @@ +import { requestUrl } from 'obsidian'; +import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; + +interface GeniusSearchHit { + result: { + id: number; + title: string; + url: string; + primary_artist: { name: string }; + }; +} + +interface GeniusSearchResponse { + response: { + hits: GeniusSearchHit[]; + }; +} + +const LYRICS_DIV_RE = /]*data-lyrics-container="true"[^>]*>([\s\S]*?)<\/div>/gi; +const LYRICS_CLASS_FALLBACK_RE = /]*class="[^"]*Lyrics__Container[^"]*"[^>]*>([\s\S]*?)<\/div>/gi; + +function stripHtmlToPlainLyrics(fragment: string): string { + return fragment + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/\n{3,}/g, '\n\n') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); +} + +function extractLyricsFromGeniusHtml(html: string): string { + const chunks: string[] = []; + let m: RegExpExecArray | null; + while ((m = LYRICS_DIV_RE.exec(html)) !== null) { + chunks.push(stripHtmlToPlainLyrics(m[1])); + } + LYRICS_DIV_RE.lastIndex = 0; + if (chunks.length > 0) { + return chunks.filter(Boolean).join('\n\n').trim(); + } + while ((m = LYRICS_CLASS_FALLBACK_RE.exec(html)) !== null) { + chunks.push(stripHtmlToPlainLyrics(m[1])); + } + LYRICS_CLASS_FALLBACK_RE.lastIndex = 0; + return chunks.filter(Boolean).join('\n\n').trim(); +} + +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, + headers: { + 'User-Agent': this.userAgent, + Authorization: `Bearer ${this.accessToken.trim()}`, + }, + }); + + if (res.status !== 200) { + 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 { + const res = await requestUrl({ + url: songPageUrl, + headers: { + 'User-Agent': this.userAgent, + }, + }); + + if (res.status !== 200) { + console.warn(`MDB | Genius song page returned ${res.status}`); + return ''; + } + + return extractLyricsFromGeniusHtml(res.text); + } +} diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzBandAPI.ts new file mode 100644 index 00000000..94301aac --- /dev/null +++ b/src/api/apis/MusicBrainzBandAPI.ts @@ -0,0 +1,235 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { BandModel } from '../../models/BandModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +interface ArtistTag { + name: string; + count: number; +} + +interface ArtistGenre { + name: string; +} + +interface ArtistSearchArtist { + id: string; + name: string; + 'life-span'?: { begin?: string; end?: string }; + country?: string; + disambiguation?: string; +} + +interface ArtistSearchResponse { + artists: ArtistSearchArtist[]; +} + +interface ArtistDetailResponse { + id: string; + name: string; + type?: string; + 'life-span'?: { begin?: string; end?: string }; + country?: string; + disambiguation?: string; + tags?: ArtistTag[]; + genres?: ArtistGenre[]; + relations?: { url?: { resource: string } | null; type: string }[]; +} + +interface ReleaseGroupListItem { + id: string; + title: string; + 'primary-type': string; + 'secondary-types'?: string[]; + 'first-release-date'?: string; +} + +interface ReleaseGroupBrowseResponse { + 'release-groups': ReleaseGroupListItem[]; +} + +const EXCLUDED_SECONDARY_TYPES = new Set([ + 'Compilation', + 'Live', + 'Remix', + 'Soundtrack', + 'Spokenword', + 'Interview', + 'Audio drama', + 'DJ-mix', + 'Mixtape/Street', + 'Demo', + 'Field recording', +]); + +export class MusicBrainzBandAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MusicBrainz Band API'; + this.apiDescription = 'MusicBrainz artist search and studio album discography.'; + this.apiUrl = 'https://musicbrainz.org/'; + this.types = [MediaType.Band]; + } + + private mbHeaders(): Record { + return { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }; + } + + private async throttleMs(ms: number): Promise { + await new Promise(resolve => setTimeout(resolve, ms)); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://musicbrainz.org/ws/2/artist?query=${encodeURIComponent(title)}&limit=20&fmt=json`; + const fetchData = await requestUrl({ + url: searchUrl, + headers: this.mbHeaders(), + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json) as ArtistSearchResponse; + const ret: MediaTypeModel[] = []; + + for (const artist of data.artists ?? []) { + const begin = artist['life-span']?.begin; + ret.push( + new BandModel({ + type: 'band', + title: artist.name, + englishTitle: artist.name, + year: begin ? (begin.split('-')[0] ?? '') : '', + beginYear: begin ? (begin.split('-')[0] ?? '') : '', + releaseDate: '', + dataSource: this.apiName, + url: 'https://musicbrainz.org/artist/' + artist.id, + id: artist.id, + country: artist.country ?? '', + disambiguation: artist.disambiguation ?? '', + genres: [], + image: '', + officialWebsite: '', + subType: 'band', + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const artistUrl = `https://musicbrainz.org/ws/2/artist/${encodeURIComponent(id)}?inc=tags+genres+url-rels&fmt=json`; + const res = await requestUrl({ + url: artistUrl, + headers: this.mbHeaders(), + }); + + if (res.status !== 200) { + throw Error(`MDB | Received status code ${res.status} from ${this.apiName}.`); + } + + const artist = (await res.json) as ArtistDetailResponse; + const begin = artist['life-span']?.begin; + const beginYear = begin ? (begin.split('-')[0] ?? '') : ''; + + let officialWebsite = ''; + for (const rel of artist.relations ?? []) { + if (rel.type === 'official homepage' && rel.url?.resource) { + officialWebsite = rel.url.resource; + break; + } + } + + return new BandModel({ + type: 'band', + title: artist.name, + englishTitle: artist.name, + year: beginYear, + beginYear, + releaseDate: begin ? (this.plugin.dateFormatter.format(begin, this.apiDateFormat) ?? 'unknown') : '', + dataSource: this.apiName, + url: 'https://musicbrainz.org/artist/' + artist.id, + id: artist.id, + country: artist.country ?? '', + disambiguation: artist.disambiguation ?? '', + genres: [...new Set([...(artist.genres?.map(g => g.name) ?? []), ...(artist.tags?.map(t => t.name) ?? [])])], + image: '', + officialWebsite, + subType: 'band', + userData: { + personalRating: 0, + }, + }); + } + + /** + * Lists release group MBIDs for studio albums (primary type album, excluding live/compilations/etc.). + */ + async listStudioAlbumReleaseGroupIds(artistId: string): Promise { + const collected: { id: string; date: string }[] = []; + let offset = 0; + const limit = 100; + + while (true) { + await this.throttleMs(1100); + const url = `https://musicbrainz.org/ws/2/release-group?artist=${encodeURIComponent(artistId)}&type=album&fmt=json&limit=${limit}&offset=${offset}`; + + const res = await requestUrl({ + url, + headers: this.mbHeaders(), + }); + + if (res.status !== 200) { + throw Error(`MDB | Received status code ${res.status} browsing release groups.`); + } + + const data = (await res.json) as ReleaseGroupBrowseResponse; + const groups = data['release-groups'] ?? []; + if (groups.length === 0) { + break; + } + + for (const rg of groups) { + if (rg['primary-type'] !== 'Album') { + continue; + } + const secondary = rg['secondary-types'] ?? []; + if (secondary.some(t => EXCLUDED_SECONDARY_TYPES.has(t))) { + continue; + } + collected.push({ + id: rg.id, + date: rg['first-release-date'] ?? '', + }); + } + + offset += limit; + if (groups.length < limit) { + break; + } + } + + collected.sort((a, b) => a.date.localeCompare(b.date)); + return [...new Set(collected.map(c => c.id))]; + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MusicBrainzBandAPI_disabledMediaTypes; + } +} diff --git a/src/main.ts b/src/main.ts index 4762664f..4ef3c1cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import type { TFile } from 'obsidian'; import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian'; import { requestUrl, normalizePath } from 'obsidian'; -import type { MediaType } from 'src/utils/MediaType'; +import { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { ComicVineAPI } from './api/apis/ComicVineAPI'; @@ -11,6 +11,7 @@ import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; +import { MusicBrainzBandAPI } from './api/apis/MusicBrainzBandAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { RAWGAPI } from './api/apis/RAWGAPI'; @@ -20,12 +21,17 @@ import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; import { VNDBAPI } from './api/apis/VNDBAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; +import { GeniusClient } from './api/GeniusClient'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; +import type { BandModel } from './models/BandModel'; import type { MediaTypeModel } from './models/MediaTypeModel'; +import type { MusicReleaseModel } from './models/MusicReleaseModel'; import type { SeasonModel } from './models/SeasonModel'; +import { SongModel } from './models/SongModel'; import { migrateLegacyApiKeysToSecretStorage, stripPlaintextApiKeysFromSettings } from './settings/apiSecretHelpers'; +import { API_SECRET_IDS } from './settings/apiSecretIds'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; @@ -66,6 +72,7 @@ export default class MediaDbPlugin extends Plugin { this.apiManager.registerAPI(new MALAPIManga(this)); this.apiManager.registerAPI(new WikipediaAPI(this)); this.apiManager.registerAPI(new MusicBrainzAPI(this)); + this.apiManager.registerAPI(new MusicBrainzBandAPI(this)); this.apiManager.registerAPI(new SteamAPI(this)); this.apiManager.registerAPI(new TMDBSeriesAPI(this)); this.apiManager.registerAPI(new TMDBSeasonAPI(this)); @@ -426,10 +433,17 @@ export default class MediaDbPlugin extends Plugin { } async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - // Create notes in parallel for better performance + const hasBand = models.some(m => m.getMediaType() === MediaType.Band); + + if (hasBand) { + for (const model of models) { + await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); + } + return; + } + const results = await Promise.allSettled(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }))); - // Report any failures const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0) { console.warn('MDB | Some notes failed to create:', failures); @@ -456,10 +470,19 @@ export default class MediaDbPlugin extends Plugin { } async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + if (mediaTypeModel.getMediaType() === MediaType.Band) { + await this.importBandDiscography(mediaTypeModel as BandModel, options); + return; + } + + await this.createStandardMediaDbNoteFromModel(mediaTypeModel, options); + } + + private async createStandardMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { try { console.debug('MDB | creating new note'); - options.openNote = this.settings.openNoteInNewTab; + options.openNote ??= this.settings.openNoteInNewTab; if (this.settings.imageDownload) { await this.downloadImageForMediaModel(mediaTypeModel); @@ -480,6 +503,100 @@ export default class MediaDbPlugin extends Plugin { } } + private async importBandDiscography(band: BandModel, options: CreateNoteOptions): Promise { + try { + const geniusToken = this.app.secretStorage.getSecret(API_SECRET_IDS.genius); + const genius = new GeniusClient(geniusToken ?? undefined); + if (!genius.isConfigured()) { + new Notice('Band import: add a Genius API access token in settings to fetch lyrics.'); + } + + const bandApi = this.apiManager.getApiByName('MusicBrainz Band API') as MusicBrainzBandAPI | undefined; + const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; + if (!bandApi || !musicBrainzApi) { + new Notice('MusicBrainz APIs not available.'); + return; + } + + const childOptions: CreateNoteOptions = { + attachTemplate: true, + openNote: false, + attachFile: undefined, + folder: undefined, + }; + + await this.createStandardMediaDbNoteFromModel(band, { ...options }); + + let releaseGroupIds: string[]; + try { + releaseGroupIds = await bandApi.listStudioAlbumReleaseGroupIds(band.id); + } catch (e) { + new Notice(`Could not load albums: ${e}`); + return; + } + + new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${band.title}…`); + + for (const rgId of releaseGroupIds) { + await new Promise(r => setTimeout(r, 1100)); + let release: MusicReleaseModel; + try { + const model = await musicBrainzApi.getById(rgId); + release = model as MusicReleaseModel; + } catch (e) { + console.warn(`MDB | Skipping release group ${rgId}:`, e); + continue; + } + + await this.createStandardMediaDbNoteFromModel(release, { ...childOptions }); + + for (const track of release.tracks) { + let lyrics = ''; + let geniusUrl = ''; + if (genius.isConfigured()) { + await new Promise(r => setTimeout(r, 500)); + const hit = await genius.searchFirstSongHit(`${band.title} ${track.title}`); + if (hit) { + geniusUrl = hit.url; + await new Promise(r => setTimeout(r, 600)); + lyrics = await genius.fetchLyricsFromSongPage(hit.url); + } + } + + const song = new SongModel({ + type: 'song', + title: track.title, + englishTitle: track.title, + year: release.year, + releaseDate: release.releaseDate, + dataSource: genius.isConfigured() ? 'MusicBrainz / Genius' : 'MusicBrainz', + url: geniusUrl || release.url, + id: `${release.id}-t${track.number}`, + image: release.image, + subType: 'song', + genres: release.genres ?? [], + artists: release.artists.length > 0 ? release.artists : [band.title], + albumTitle: release.title, + albumReleaseGroupId: release.id, + trackNumber: track.number, + duration: track.duration, + featuredArtists: track.featuredArtists, + geniusUrl, + lyrics, + userData: { personalRating: 0 }, + }); + + await this.createStandardMediaDbNoteFromModel(song, { ...childOptions }); + } + } + + new Notice(`Finished band import for ${band.title}.`); + } catch (e) { + console.warn(e); + new Notice(`${e}`); + } + } + /** * Tries to download the image for a media model. * @@ -545,9 +662,16 @@ export default class MediaDbPlugin extends Plugin { ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); + if (mediaTypeModel.getMediaType() === MediaType.Song) { + const song = mediaTypeModel as SongModel; + const body = (song.lyrics ?? '').trim(); + fileContent += `\n\n# Lyrics\n\n${body.length > 0 ? body : '_Lyrics not found._'}\n`; + } + if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { // Include the media variable in all templater commands by using a top level JavaScript execution command. - fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + const mediaJson = JSON.stringify(mediaTypeModel, (key, value: unknown) => (key === 'lyrics' ? undefined : value)); + fileContent = `---\n<%* const media = ${mediaJson} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; } else { fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; } @@ -735,4 +859,4 @@ export default class MediaDbPlugin extends Plugin { await this.saveData(this.settings); } -} \ No newline at end of file +} diff --git a/src/modals/MediaDbSearchModal.ts b/src/modals/MediaDbSearchModal.ts index 986cb8a8..8332f5ab 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/src/modals/MediaDbSearchModal.ts @@ -5,7 +5,7 @@ import type { MediaType } from '../utils/MediaType'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; -import { unCamelCase } from '../utils/Utils'; +import { mediaTypeDisplayName } from '../utils/Utils'; export class MediaDbSearchModal extends Modal { plugin: MediaDbPlugin; @@ -92,12 +92,12 @@ export class MediaDbSearchModal extends Modal { const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text' }); + apiToggleTextWrapper.createEl('span', { text: mediaTypeDisplayName(mediaType), cls: 'media-db-plugin-list-text' }); const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); - apiToggleComponent.setTooltip(unCamelCase(mediaType)); + apiToggleComponent.setTooltip(mediaTypeDisplayName(mediaType)); apiToggleComponent.setValue(this.selectedTypes.contains(mediaType)); if (apiToggleComponent.getValue()) { currentToggle = apiToggleComponent; diff --git a/src/models/BandModel.ts b/src/models/BandModel.ts new file mode 100644 index 00000000..e5ec1a24 --- /dev/null +++ b/src/models/BandModel.ts @@ -0,0 +1,60 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type BandData = ModelToData; + +export class BandModel extends MediaTypeModel { + genres: string[]; + country: string; + image: string; + officialWebsite: string; + disambiguation: string; + beginYear: string; + releaseDate: string; + + userData: { + personalRating: number; + }; + + constructor(obj: BandData) { + super(); + + this.genres = []; + this.country = ''; + this.image = ''; + this.officialWebsite = ''; + this.disambiguation = ''; + this.beginYear = ''; + this.releaseDate = ''; + + this.userData = { + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + this.releaseDate = obj.releaseDate ?? ''; + } + + getTags(): string[] { + return [mediaDbTag, 'music', 'band']; + } + + getMediaType(): MediaType { + return MediaType.Band; + } + + getSummary(): string { + let summary = this.title; + if (this.beginYear) summary += ` (formed ${this.beginYear})`; + if (this.disambiguation) summary += ` — ${this.disambiguation}`; + return summary; + } +} diff --git a/src/models/SongModel.ts b/src/models/SongModel.ts new file mode 100644 index 00000000..b57fb7bf --- /dev/null +++ b/src/models/SongModel.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 SongData = ModelToData; + +export class SongModel extends MediaTypeModel { + genres: string[]; + artists: string[]; + albumTitle: string; + albumReleaseGroupId: string; + trackNumber: number; + duration: string; + featuredArtists: string[]; + geniusUrl: string; + lyrics: string; + image: string; + releaseDate: string; + + userData: { + personalRating: number; + }; + + constructor(obj: SongData) { + super(); + + this.genres = []; + this.artists = []; + this.albumTitle = ''; + this.albumReleaseGroupId = ''; + this.trackNumber = 0; + this.duration = ''; + this.featuredArtists = []; + this.geniusUrl = ''; + this.lyrics = ''; + this.image = ''; + this.releaseDate = ''; + + this.userData = { + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + this.trackNumber = obj.trackNumber ?? 0; + this.albumTitle = obj.albumTitle ?? ''; + this.albumReleaseGroupId = obj.albumReleaseGroupId ?? ''; + this.duration = obj.duration ?? ''; + this.featuredArtists = obj.featuredArtists ?? []; + this.geniusUrl = obj.geniusUrl ?? ''; + this.lyrics = obj.lyrics ?? ''; + this.releaseDate = obj.releaseDate ?? ''; + } + + getTags(): string[] { + return [mediaDbTag, 'music', 'song']; + } + + getMediaType(): MediaType { + return MediaType.Song; + } + + getSummary(): string { + const albumPart = this.albumTitle ? ` — ${this.albumTitle}` : ''; + const artists = this.artists.length > 0 ? this.artists.join(', ') : ''; + return `${this.title}${albumPart}${artists ? ` (${artists})` : ''}`; + } + + getWithOutUserData(): Record { + const copy = super.getWithOutUserData(); + delete copy.lyrics; + return copy; + } +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d95f7606..8ec18191 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -5,7 +5,7 @@ import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { fragWithHTML, unCamelCase } from '../utils/Utils'; +import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; import type { ApiSecretId } from './apiSecretIds'; import { API_SECRET_IDS } from './apiSecretIds'; import type { PropertyMappingModelData } from './PropertyMapping'; @@ -16,24 +16,28 @@ import { FolderSuggest } from './suggesters/FolderSuggest'; function mediaTypeTabIcon(mediaType: MediaType): IconName { switch (mediaType) { - case MediaType.Movie: - return 'film'; - case MediaType.Series: - return 'tv'; - case MediaType.Season: - return 'calendar-range'; + case MediaType.Band: + return 'mic-2'; + case MediaType.BoardGame: + return 'dice-3'; + case MediaType.Book: + return 'book-marked'; case MediaType.ComicManga: return 'book-open'; case MediaType.Game: return 'gamepad-2'; - case MediaType.Wiki: - return 'library-big'; + case MediaType.Movie: + return 'film'; case MediaType.MusicRelease: return 'disc-3'; - case MediaType.BoardGame: - return 'dice-3'; - case MediaType.Book: - return 'book-marked'; + case MediaType.Season: + return 'calendar-range'; + case MediaType.Series: + return 'tv'; + case MediaType.Song: + return 'music-4'; + case MediaType.Wiki: + return 'library-big'; } } @@ -66,6 +70,7 @@ export interface MediaDbPluginSettings { MALAPIManga_disabledMediaTypes: MediaType[]; MobyGamesAPI_disabledMediaTypes: MediaType[]; MusicBrainzAPI_disabledMediaTypes: MediaType[]; + MusicBrainzBandAPI_disabledMediaTypes: MediaType[]; OMDbAPI_disabledMediaTypes: MediaType[]; OpenLibraryAPI_disabledMediaTypes: MediaType[]; SteamAPI_disabledMediaTypes: MediaType[]; @@ -82,6 +87,8 @@ export interface MediaDbPluginSettings { gameTemplate: string; wikiTemplate: string; musicReleaseTemplate: string; + bandTemplate: string; + songTemplate: string; boardgameTemplate: string; bookTemplate: string; @@ -92,6 +99,8 @@ export interface MediaDbPluginSettings { gameFileNameTemplate: string; wikiFileNameTemplate: string; musicReleaseFileNameTemplate: string; + bandFileNameTemplate: string; + songFileNameTemplate: string; boardgameFileNameTemplate: string; bookFileNameTemplate: string; @@ -102,6 +111,8 @@ export interface MediaDbPluginSettings { gameFolder: string; wikiFolder: string; musicReleaseFolder: string; + bandFolder: string; + songFolder: string; boardgameFolder: string; bookFolder: string; @@ -115,6 +126,8 @@ export interface MediaDbPluginSettings { gamePropertyConversionRules: string; wikiPropertyConversionRules: string; musicReleasePropertyConversionRules: string; + bandPropertyConversionRules: string; + songPropertyConversionRules: string; boardgamePropertyConversionRules: string; bookPropertyConversionRules: string; } @@ -131,37 +144,41 @@ class MediaTypeMappedSettings { getTemplate(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Movie: - return settings.movieTemplate; - case MediaType.Series: - return settings.seriesTemplate; - case MediaType.Season: - return settings.seasonTemplate; + case MediaType.Band: + return settings.bandTemplate; + case MediaType.BoardGame: + return settings.boardgameTemplate; + case MediaType.Book: + return settings.bookTemplate; case MediaType.ComicManga: return settings.mangaTemplate; case MediaType.Game: return settings.gameTemplate; - case MediaType.Wiki: - return settings.wikiTemplate; + case MediaType.Movie: + return settings.movieTemplate; case MediaType.MusicRelease: return settings.musicReleaseTemplate; - case MediaType.BoardGame: - return settings.boardgameTemplate; - case MediaType.Book: - return settings.bookTemplate; + case MediaType.Season: + return settings.seasonTemplate; + case MediaType.Series: + return settings.seriesTemplate; + case MediaType.Song: + return settings.songTemplate; + case MediaType.Wiki: + return settings.wikiTemplate; } } setTemplate(settings: MediaDbPluginSettings, template: string): void { switch (this.mediaType) { - case MediaType.Movie: - settings.movieTemplate = template; + case MediaType.Band: + settings.bandTemplate = template; break; - case MediaType.Series: - settings.seriesTemplate = template; + case MediaType.BoardGame: + settings.boardgameTemplate = template; break; - case MediaType.Season: - settings.seasonTemplate = template; + case MediaType.Book: + settings.bookTemplate = template; break; case MediaType.ComicManga: settings.mangaTemplate = template; @@ -169,54 +186,64 @@ class MediaTypeMappedSettings { case MediaType.Game: settings.gameTemplate = template; break; - case MediaType.Wiki: - settings.wikiTemplate = template; + case MediaType.Movie: + settings.movieTemplate = template; break; case MediaType.MusicRelease: settings.musicReleaseTemplate = template; break; - case MediaType.BoardGame: - settings.boardgameTemplate = template; + case MediaType.Season: + settings.seasonTemplate = template; break; - case MediaType.Book: - settings.bookTemplate = template; + case MediaType.Series: + settings.seriesTemplate = template; + break; + case MediaType.Song: + settings.songTemplate = template; + break; + case MediaType.Wiki: + settings.wikiTemplate = template; break; } } getFileNameTemplate(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Movie: - return settings.movieFileNameTemplate; - case MediaType.Series: - return settings.seriesFileNameTemplate; - case MediaType.Season: - return settings.seasonFileNameTemplate; + case MediaType.Band: + return settings.bandFileNameTemplate; + case MediaType.BoardGame: + return settings.boardgameFileNameTemplate; + case MediaType.Book: + return settings.bookFileNameTemplate; case MediaType.ComicManga: return settings.mangaFileNameTemplate; case MediaType.Game: return settings.gameFileNameTemplate; - case MediaType.Wiki: - return settings.wikiFileNameTemplate; + case MediaType.Movie: + return settings.movieFileNameTemplate; case MediaType.MusicRelease: return settings.musicReleaseFileNameTemplate; - case MediaType.BoardGame: - return settings.boardgameFileNameTemplate; - case MediaType.Book: - return settings.bookFileNameTemplate; + case MediaType.Season: + return settings.seasonFileNameTemplate; + case MediaType.Series: + return settings.seriesFileNameTemplate; + case MediaType.Song: + return settings.songFileNameTemplate; + case MediaType.Wiki: + return settings.wikiFileNameTemplate; } } setFileNameTemplate(settings: MediaDbPluginSettings, template: string): void { switch (this.mediaType) { - case MediaType.Movie: - settings.movieFileNameTemplate = template; + case MediaType.Band: + settings.bandFileNameTemplate = template; break; - case MediaType.Series: - settings.seriesFileNameTemplate = template; + case MediaType.BoardGame: + settings.boardgameFileNameTemplate = template; break; - case MediaType.Season: - settings.seasonFileNameTemplate = template; + case MediaType.Book: + settings.bookFileNameTemplate = template; break; case MediaType.ComicManga: settings.mangaFileNameTemplate = template; @@ -224,54 +251,64 @@ class MediaTypeMappedSettings { case MediaType.Game: settings.gameFileNameTemplate = template; break; - case MediaType.Wiki: - settings.wikiFileNameTemplate = template; + case MediaType.Movie: + settings.movieFileNameTemplate = template; break; case MediaType.MusicRelease: settings.musicReleaseFileNameTemplate = template; break; - case MediaType.BoardGame: - settings.boardgameFileNameTemplate = template; + case MediaType.Season: + settings.seasonFileNameTemplate = template; break; - case MediaType.Book: - settings.bookFileNameTemplate = template; + case MediaType.Series: + settings.seriesFileNameTemplate = template; + break; + case MediaType.Song: + settings.songFileNameTemplate = template; + break; + case MediaType.Wiki: + settings.wikiFileNameTemplate = template; break; } } getFolder(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Movie: - return settings.movieFolder; - case MediaType.Series: - return settings.seriesFolder; - case MediaType.Season: - return settings.seasonFolder; + case MediaType.Band: + return settings.bandFolder; + case MediaType.BoardGame: + return settings.boardgameFolder; + case MediaType.Book: + return settings.bookFolder; case MediaType.ComicManga: return settings.mangaFolder; case MediaType.Game: return settings.gameFolder; - case MediaType.Wiki: - return settings.wikiFolder; + case MediaType.Movie: + return settings.movieFolder; case MediaType.MusicRelease: return settings.musicReleaseFolder; - case MediaType.BoardGame: - return settings.boardgameFolder; - case MediaType.Book: - return settings.bookFolder; + case MediaType.Season: + return settings.seasonFolder; + case MediaType.Series: + return settings.seriesFolder; + case MediaType.Song: + return settings.songFolder; + case MediaType.Wiki: + return settings.wikiFolder; } } setFolder(settings: MediaDbPluginSettings, folder: string): void { switch (this.mediaType) { - case MediaType.Movie: - settings.movieFolder = folder; + case MediaType.Band: + settings.bandFolder = folder; break; - case MediaType.Series: - settings.seriesFolder = folder; + case MediaType.BoardGame: + settings.boardgameFolder = folder; break; - case MediaType.Season: - settings.seasonFolder = folder; + case MediaType.Book: + settings.bookFolder = folder; break; case MediaType.ComicManga: settings.mangaFolder = folder; @@ -279,17 +316,23 @@ class MediaTypeMappedSettings { case MediaType.Game: settings.gameFolder = folder; break; - case MediaType.Wiki: - settings.wikiFolder = folder; + case MediaType.Movie: + settings.movieFolder = folder; break; case MediaType.MusicRelease: settings.musicReleaseFolder = folder; break; - case MediaType.BoardGame: - settings.boardgameFolder = folder; + case MediaType.Season: + settings.seasonFolder = folder; break; - case MediaType.Book: - settings.bookFolder = folder; + case MediaType.Series: + settings.seriesFolder = folder; + break; + case MediaType.Song: + settings.songFolder = folder; + break; + case MediaType.Wiki: + settings.wikiFolder = folder; break; } } @@ -324,6 +367,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { MALAPIManga_disabledMediaTypes: [], MobyGamesAPI_disabledMediaTypes: [], MusicBrainzAPI_disabledMediaTypes: [], + MusicBrainzBandAPI_disabledMediaTypes: [], OMDbAPI_disabledMediaTypes: [], OpenLibraryAPI_disabledMediaTypes: [], SteamAPI_disabledMediaTypes: [], @@ -340,6 +384,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameTemplate: '', wikiTemplate: '', musicReleaseTemplate: '', + bandTemplate: '', + songTemplate: '', boardgameTemplate: '', bookTemplate: '', @@ -350,6 +396,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})', + bandFileNameTemplate: '{{ title }}', + songFileNameTemplate: '{{ trackNumber }}. {{ title }} ({{ albumTitle }})', boardgameFileNameTemplate: '{{ title }} ({{ year }})', bookFileNameTemplate: '{{ title }} ({{ year }})', @@ -360,6 +408,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameFolder: 'Media DB/games', wikiFolder: 'Media DB/wiki', musicReleaseFolder: 'Media DB/music', + bandFolder: 'Media DB/bands', + songFolder: 'Media DB/music/songs', boardgameFolder: 'Media DB/boardgames', bookFolder: 'Media DB/books', @@ -373,6 +423,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gamePropertyConversionRules: '', wikiPropertyConversionRules: '', musicReleasePropertyConversionRules: '', + bandPropertyConversionRules: '', + songPropertyConversionRules: '', boardgamePropertyConversionRules: '', bookPropertyConversionRules: '', }; @@ -427,18 +479,141 @@ export class MediaDbSettingTab extends PluginSettingTab { } private addApiSecretSetting(group: SettingGroup, name: string, description: string, secretId: ApiSecretId): void { - group.addSetting(setting => - void setting.setName(name).setDesc(description).addComponent(el => { - const component = new SecretComponent(this.app, el); - component - .setValue(this.app.secretStorage.getSecret(secretId) ?? '') - .onChange(value => { - this.app.secretStorage.setSecret(secretId, value); - void this.plugin.saveSettings(); - }); - return component; - }), + group.addSetting( + setting => + void setting + .setName(name) + .setDesc(description) + .addComponent(el => { + const component = new SecretComponent(this.app, el); + component.setValue(this.app.secretStorage.getSecret(secretId) ?? '').onChange(value => { + this.app.secretStorage.setSecret(secretId, value); + void this.plugin.saveSettings(); + }); + return component; + }), + ); + } + + private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Band, MediaType.MusicRelease, MediaType.Song]; + + private static readonly LEGACY_MUSIC_TAB_IDS: ReadonlySet = new Set(['media-band', 'media-musicRelease', 'media-song']); + + private renderMediaTypeSection( + panel: HTMLElement, + mediaTypeSetting: MediaTypeMappedSettings, + mediaTypeApiMap: Map, + options?: { sectionHeading?: string }, + ): void { + const mediaType = mediaTypeSetting.mediaType; + const descNoun = options?.sectionHeading?.toLowerCase() ?? mediaTypeDisplayName(mediaType).toLowerCase(); + + if (options?.sectionHeading) { + panel.createEl('h3', { text: options.sectionHeading }); + } + + const mediaTypeGroup = new SettingGroup(panel); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Import folder') + .setDesc(`Where newly imported ${descNoun} notes should be placed.`) + .addSearch(cb => { + const suggester = new FolderSuggest(this.app, cb.inputEl); + suggester.onSelect(folder => { + cb.setValue(folder.path); + mediaTypeSetting.setFolder(this.plugin.settings, folder.path); + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) + .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setFolder(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), ); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Template') + .setDesc(`Template file used when creating a new ${descNoun} note.`) + .addSearch(cb => { + const suggester = new FileSuggest(this.app, cb.inputEl); + suggester.onSelect(file => { + cb.setValue(file.path); + mediaTypeSetting.setTemplate(this.plugin.settings, file.path); + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(`Example: ${descNoun.replace(/ /g, '')}Template.md`) + .setValue(mediaTypeSetting.getTemplate(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setTemplate(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('File name template') + .setDesc(`File name template for new ${descNoun} notes.`) + .addText(cb => { + cb.setPlaceholder(`Example: ${mediaTypeSetting.getFileNameTemplate(DEFAULT_SETTINGS)}`) + .setValue(mediaTypeSetting.getFileNameTemplate(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setFileNameTemplate(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + + const apis = mediaTypeApiMap.get(mediaType) ?? []; + if (apis.length > 1) { + for (const apiName of apis) { + const api = this.plugin.apiManager.apis.find(a => a.apiName === apiName); + if (api) { + const disabledMediaTypes = api.getDisabledMediaTypes(); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName(apiName) + .setDesc(`Use ${apiName} for ${descNoun} search and import.`) + .addToggle(cb => { + cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => { + if (data) { + const index = disabledMediaTypes.indexOf(mediaType); + if (index != -1) { + disabledMediaTypes.splice(index, 1); + } + } else { + disabledMediaTypes.push(mediaType); + } + void this.plugin.saveSettings(); + }); + }), + ); + } + } + } + } + + private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Band), mediaTypeApiMap, { sectionHeading: 'Band' }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { sectionHeading: 'Album' }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { sectionHeading: 'Song' }); } display(): void { @@ -487,277 +662,175 @@ export class MediaDbSettingTab extends PluginSettingTab { const generalGroup = new SettingGroup(panel); generalGroup.addSetting( - setting => - void setting - .setName('SFW filter') - .setDesc('Only shows SFW results for APIs that offer filtering.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.sfwFilter).onChange(data => { - this.plugin.settings.sfwFilter = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Resolve {{ tags }} in templates') - .setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.templates).onChange(data => { - this.plugin.settings.templates = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Date format') - .setDesc( - fragWithHTML( - "Your custom date format. Use 'YYYY-MM-DD' for example.
" + - "For more syntax, refer to format reference.
" + - "Your current syntax looks like this: " + - this.plugin.dateFormatter.getPreview() + - '', - ), - ) - .addText(cb => { - cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) - .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) - .onChange(data => { - const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; - this.plugin.settings.customDateFormat = newDateFormat; - const previewEl = document.getElementById('media-db-dateformat-preview'); - if (previewEl) { - previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview - } + setting => + void setting + .setName('SFW filter') + .setDesc('Only shows SFW results for APIs that offer filtering.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.sfwFilter).onChange(data => { + this.plugin.settings.sfwFilter = data; void this.plugin.saveSettings(); }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Open note in new tab') - .setDesc('Open the newly created note in a new tab.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => { - this.plugin.settings.openNoteInNewTab = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Use default front matter') - .setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { - this.plugin.settings.useDefaultFrontMatter = data; - void this.plugin.saveSettings(); - // Redraw settings to display/remove the property mappings - this.display(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Enable Templater integration') - .setDesc( - 'Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.', - ) - .addToggle(cb => { - cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { - this.plugin.settings.enableTemplaterIntegration = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Download images') - .setDesc('Downloads images for new notes in the folder below') - .addToggle(cb => { - cb.setValue(this.plugin.settings.imageDownload).onChange(data => { - this.plugin.settings.imageDownload = data; - void this.plugin.saveSettings(); - }); - }), - ); + }), + ); - generalGroup.addSetting( - setting => - void setting - .setName('Image folder') - .setDesc('Where downloaded images should be stored.') - .addSearch(cb => { - const suggester = new FolderSuggest(this.app, cb.inputEl); - suggester.onSelect(folder => { - cb.setValue(folder.path); - this.plugin.settings.imageFolder = folder.path; - void this.plugin.saveSettings(); - suggester.close(); - }); - cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder) - .setValue(this.plugin.settings.imageFolder) - .onChange(data => { - this.plugin.settings.imageFolder = data; + generalGroup.addSetting( + setting => + void setting + .setName('Resolve {{ tags }} in templates') + .setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.templates).onChange(data => { + this.plugin.settings.templates = data; void this.plugin.saveSettings(); }); - }), - ); - }); - - addTab('api-keys', 'API keys', 'key', panel => { - const apiKeyGroup = new SettingGroup(panel); - - this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', API_SECRET_IDS.omdb); - this.addApiSecretSetting( - apiKeyGroup, - 'TMDB API Token', - 'API Read Access Token for "https://www.themoviedb.org".', - API_SECRET_IDS.tmdb, - ); - this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', API_SECRET_IDS.mobyGames); - this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', API_SECRET_IDS.giantBomb); - this.addApiSecretSetting( - apiKeyGroup, - 'IGDB Client ID', - 'Client ID for IGDB API (Required for Twitch OAuth).', - API_SECRET_IDS.igdbClientId, - ); - this.addApiSecretSetting( - apiKeyGroup, - 'IGDB Client Secret', - 'Client Secret for IGDB API.', - API_SECRET_IDS.igdbClientSecret, - ); - this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', API_SECRET_IDS.rawg); - this.addApiSecretSetting( - apiKeyGroup, - 'Comic Vine Key', - 'API key for "www.comicvine.gamespot.com".', - API_SECRET_IDS.comicVine, + }), ); - this.addApiSecretSetting( - apiKeyGroup, - 'Boardgame Geek Key', - 'API key for "www.boardgamegeek.com".', - API_SECRET_IDS.boardgameGeek, + + generalGroup.addSetting( + setting => + void setting + .setName('Date format') + .setDesc( + fragWithHTML( + "Your custom date format. Use 'YYYY-MM-DD' for example.
" + + "For more syntax, refer to format reference.
" + + "Your current syntax looks like this: " + + this.plugin.dateFormatter.getPreview() + + '', + ), + ) + .addText(cb => { + cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) + .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) + .onChange(data => { + const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; + this.plugin.settings.customDateFormat = newDateFormat; + const previewEl = document.getElementById('media-db-dateformat-preview'); + if (previewEl) { + previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview + } + void this.plugin.saveSettings(); + }); + }), ); - }); - for (const mediaTypeSetting of mediaTypeSettings) { - const mediaType = mediaTypeSetting.mediaType; - const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); + generalGroup.addSetting( + setting => + void setting + .setName('Open note in new tab') + .setDesc('Open the newly created note in a new tab.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => { + this.plugin.settings.openNoteInNewTab = data; + void this.plugin.saveSettings(); + }); + }), + ); - addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { - const mediaTypeGroup = new SettingGroup(panel); - const mediaTypeNameLower = mediaTypeName.toLowerCase(); + generalGroup.addSetting( + setting => + void setting + .setName('Use default front matter') + .setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { + this.plugin.settings.useDefaultFrontMatter = data; + void this.plugin.saveSettings(); + // Redraw settings to display/remove the property mappings + this.display(); + }); + }), + ); - // Folder - mediaTypeGroup.addSetting( + generalGroup.addSetting( setting => void setting - .setName(`Import Folder`) - .setDesc(`Where newly imported ${mediaTypeNameLower} should be placed.`) - .addSearch(cb => { - const suggester = new FolderSuggest(this.app, cb.inputEl); - suggester.onSelect(folder => { - cb.setValue(folder.path); - mediaTypeSetting.setFolder(this.plugin.settings, folder.path); + .setName('Enable Templater integration') + .setDesc( + 'Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { + this.plugin.settings.enableTemplaterIntegration = data; void this.plugin.saveSettings(); - suggester.close(); }); - cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) - .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setFolder(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); }), ); - // Template - mediaTypeGroup.addSetting( + generalGroup.addSetting( setting => void setting - .setName(`Template`) - .setDesc(`Template file to be used when creating a new note for a ${mediaTypeNameLower}.`) - .addSearch(cb => { - const suggester = new FileSuggest(this.app, cb.inputEl); - suggester.onSelect(file => { - cb.setValue(file.path); - mediaTypeSetting.setTemplate(this.plugin.settings, file.path); + .setName('Download images') + .setDesc('Downloads images for new notes in the folder below') + .addToggle(cb => { + cb.setValue(this.plugin.settings.imageDownload).onChange(data => { + this.plugin.settings.imageDownload = data; void this.plugin.saveSettings(); - suggester.close(); }); - cb.setPlaceholder(`Example: ${mediaTypeNameLower}Template.md`) - .setValue(mediaTypeSetting.getTemplate(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setTemplate(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); }), ); - // File name template - mediaTypeGroup.addSetting( + generalGroup.addSetting( setting => void setting - .setName(`File name template`) - .setDesc(`Template for the file name used when creating a new note for a ${mediaTypeNameLower}.`) - .addText(cb => { - cb.setPlaceholder(`Example: ${mediaTypeSetting.getFileNameTemplate(DEFAULT_SETTINGS)}`) - .setValue(mediaTypeSetting.getFileNameTemplate(this.plugin.settings)) + .setName('Image folder') + .setDesc('Where downloaded images should be stored.') + .addSearch(cb => { + const suggester = new FolderSuggest(this.app, cb.inputEl); + suggester.onSelect(folder => { + cb.setValue(folder.path); + this.plugin.settings.imageFolder = folder.path; + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder) + .setValue(this.plugin.settings.imageFolder) .onChange(data => { - mediaTypeSetting.setFileNameTemplate(this.plugin.settings, data); + this.plugin.settings.imageFolder = data; void this.plugin.saveSettings(); }); }), ); + }); - // APIs - const apis = mediaTypeApiMap.get(mediaType) ?? []; - if (apis.length > 1) { - for (const apiName of apis) { - const api = this.plugin.apiManager.apis.find(api => api.apiName === apiName); - if (api) { - const disabledMediaTypes = api.getDisabledMediaTypes(); - - mediaTypeGroup.addSetting( - setting => - void setting - .setName(apiName) - .setDesc(`Use ${apiName} API for ${unCamelCase(mediaType)}.`) - .addToggle(cb => { - cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => { - if (data) { - const index = disabledMediaTypes.indexOf(mediaType); - if (index != -1) { - disabledMediaTypes.splice(index, 1); - } - } else { - disabledMediaTypes.push(mediaType); - } - void this.plugin.saveSettings(); - }); - }), - ); - } + addTab('api-keys', 'API keys', 'key', panel => { + const apiKeyGroup = new SettingGroup(panel); + + this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', API_SECRET_IDS.omdb); + this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', API_SECRET_IDS.tmdb); + this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', API_SECRET_IDS.mobyGames); + this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', API_SECRET_IDS.giantBomb); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', API_SECRET_IDS.igdbClientId); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client Secret', 'Client Secret for IGDB API.', API_SECRET_IDS.igdbClientSecret); + this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', API_SECRET_IDS.rawg); + this.addApiSecretSetting(apiKeyGroup, 'Comic Vine Key', 'API key for "www.comicvine.gamespot.com".', API_SECRET_IDS.comicVine); + this.addApiSecretSetting(apiKeyGroup, 'Boardgame Geek Key', 'API key for "www.boardgamegeek.com".', API_SECRET_IDS.boardgameGeek); + this.addApiSecretSetting( + apiKeyGroup, + 'Genius API access token', + 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing a band.', + API_SECRET_IDS.genius, + ); + }); + + let musicTabAdded = false; + for (const mediaTypeSetting of mediaTypeSettings) { + const mediaType = mediaTypeSetting.mediaType; + + if (MediaDbSettingTab.MUSIC_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!musicTabAdded) { + musicTabAdded = true; + addTab('media-music', 'Music', 'disc-3', panel => { + this.renderMusicSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); } + continue; } + + const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); }); } @@ -799,11 +872,13 @@ export class MediaDbSettingTab extends PluginSettingTab { } const validIds = new Set(tabEntries.map(t => t.id)); - let initialId = - this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; + let initialId = this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; + if (MediaDbSettingTab.LEGACY_MUSIC_TAB_IDS.has(initialId) && validIds.has('media-music')) { + initialId = 'media-music'; + } if (!validIds.has(initialId)) { initialId = 'general'; } selectTab(initialId); } -} \ No newline at end of file +} diff --git a/src/settings/apiSecretIds.ts b/src/settings/apiSecretIds.ts index d88ac8ba..b2999e85 100644 --- a/src/settings/apiSecretIds.ts +++ b/src/settings/apiSecretIds.ts @@ -12,6 +12,7 @@ export const API_SECRET_IDS = { rawg: 'media-db-rawg', comicVine: 'media-db-comic-vine', boardgameGeek: 'media-db-boardgame-geek', + genius: 'media-db-genius', } as const; export type ApiSecretId = (typeof API_SECRET_IDS)[keyof typeof API_SECRET_IDS]; diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index 857050fc..3d1ad335 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -1,11 +1,13 @@ export enum MediaType { - Movie = 'movie', - Series = 'series', - Season = 'season', + Band = 'band', + BoardGame = 'boardgame', + Book = 'book', ComicManga = 'comicManga', Game = 'game', + Movie = 'movie', MusicRelease = 'musicRelease', + Season = 'season', + Series = 'series', + Song = 'song', Wiki = 'wiki', - BoardGame = 'boardgame', - Book = 'book', } diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index d9685734..560700bb 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -1,5 +1,6 @@ import type { App, TFile } from 'obsidian'; import { TFolder } from 'obsidian'; +import { BandModel } from '../models/BandModel'; import { BoardGameModel } from '../models/BoardGameModel'; import { BookModel } from '../models/BookModel'; import { ComicMangaModel } from '../models/ComicMangaModel'; @@ -9,6 +10,7 @@ import { MovieModel } from '../models/MovieModel'; import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { SeasonModel } from '../models/SeasonModel'; import { SeriesModel } from '../models/SeriesModel'; +import { SongModel } from '../models/SongModel'; import { WikiModel } from '../models/WikiModel'; import type { MediaDbPluginSettings } from '../settings/Settings'; import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; @@ -17,6 +19,7 @@ import { replaceTags } from './Utils'; // All media types in alphabetical order export const MEDIA_TYPES: MediaType[] = [ + MediaType.Band, MediaType.BoardGame, MediaType.Book, MediaType.ComicManga, @@ -25,6 +28,7 @@ export const MEDIA_TYPES: MediaType[] = [ MediaType.MusicRelease, MediaType.Series, MediaType.Season, + MediaType.Song, MediaType.Wiki, ]; @@ -41,6 +45,7 @@ export class MediaTypeManager { updateTemplates(settings: MediaDbPluginSettings): void { this.mediaFileNameTemplateMap = new Map(); + this.mediaFileNameTemplateMap.set(MediaType.Band, settings.bandFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Season, settings.seasonFileNameTemplate); @@ -50,8 +55,10 @@ export class MediaTypeManager { this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.BoardGame, settings.boardgameFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Book, settings.bookFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.Song, settings.songFileNameTemplate); this.mediaTemplateMap = new Map(); + this.mediaTemplateMap.set(MediaType.Band, settings.bandTemplate); this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate); this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate); this.mediaTemplateMap.set(MediaType.Season, settings.seasonTemplate); @@ -61,10 +68,12 @@ export class MediaTypeManager { this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); this.mediaTemplateMap.set(MediaType.BoardGame, settings.boardgameTemplate); this.mediaTemplateMap.set(MediaType.Book, settings.bookTemplate); + this.mediaTemplateMap.set(MediaType.Song, settings.songTemplate); } updateFolders(settings: MediaDbPluginSettings): void { this.mediaFolderMap = new Map(); + this.mediaFolderMap.set(MediaType.Band, settings.bandFolder); this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder); this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder); this.mediaFolderMap.set(MediaType.Season, settings.seasonFolder); @@ -74,6 +83,7 @@ export class MediaTypeManager { this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); this.mediaFolderMap.set(MediaType.BoardGame, settings.boardgameFolder); this.mediaFolderMap.set(MediaType.Book, settings.bookFolder); + this.mediaFolderMap.set(MediaType.Song, settings.songFolder); } getFileName(mediaTypeModel: MediaTypeModel): string { @@ -158,6 +168,10 @@ export class MediaTypeManager { return new BoardGameModel(obj); } else if (mediaType === MediaType.Book) { return new BookModel(obj); + } else if (mediaType === MediaType.Band) { + return new BandModel(obj); + } else if (mediaType === MediaType.Song) { + return new SongModel(obj); } throw new Error(`Unknown media type: ${mediaType}`); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 9b25b34c..69546a2d 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -2,6 +2,7 @@ import { iso6392 } from 'iso-639-2'; import type { TFile, TFolder, App } from 'obsidian'; import { requestUrl } from 'obsidian'; import type { MediaTypeModel } from '../models/MediaTypeModel'; +import { MediaType } from './MediaType'; export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; @@ -223,6 +224,14 @@ export function unCamelCase(str: string): string { ); } +/** User-facing label for a media type (e.g. MusicRelease → Album). */ +export function mediaTypeDisplayName(mediaType: MediaType): string { + if (mediaType === MediaType.MusicRelease) { + return 'Album'; + } + return unCamelCase(mediaType); +} + /* eslint-disable */ export function hasTemplaterPlugin(app: App): boolean { From a334db3ff4209e0ad33a653143e602d4ece45411 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:47:52 -0700 Subject: [PATCH 05/60] Added File Tree for Songs option --- src/main.ts | 44 +++++++++++++++++++-- src/settings/Settings.ts | 83 +++++++++++++++++++++++++++++----------- 2 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4ef3c1cc..2463fb95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -503,6 +503,22 @@ export default class MediaDbPlugin extends Plugin { } } + private safeFileTreeSegment(title: string): string { + return replaceIllegalFileNameCharactersInString(title).replaceAll(/ +/g, ' ').trim(); + } + + private async ensureVaultFolder(folderPath: string): Promise { + const normalized = normalizePath(folderPath); + if (!(await this.app.vault.adapter.exists(normalized))) { + await this.app.vault.createFolder(normalized); + } + const folder = this.app.vault.getAbstractFileByPath(normalized); + if (!(folder instanceof TFolder)) { + throw new Error(`MDB | Expected folder at ${normalized}`); + } + return folder; + } + private async importBandDiscography(band: BandModel, options: CreateNoteOptions): Promise { try { const geniusToken = this.app.secretStorage.getSecret(API_SECRET_IDS.genius); @@ -518,6 +534,7 @@ export default class MediaDbPlugin extends Plugin { return; } + const useTree = this.settings.bandUseFileTreeForSongs; const childOptions: CreateNoteOptions = { attachTemplate: true, openNote: false, @@ -525,7 +542,18 @@ export default class MediaDbPlugin extends Plugin { folder: undefined, }; - await this.createStandardMediaDbNoteFromModel(band, { ...options }); + const bandBaseFolder = await this.mediaTypeManager.getFolder(band, this.app); + let bandNoteFolder = bandBaseFolder; + let albumNotesFolder = bandBaseFolder; + + if (useTree) { + const bandSeg = this.safeFileTreeSegment(band.title); + const treeRootPath = normalizePath(`${bandBaseFolder.path}/${bandSeg}`); + albumNotesFolder = await this.ensureVaultFolder(treeRootPath); + bandNoteFolder = albumNotesFolder; + } + + await this.createStandardMediaDbNoteFromModel(band, { ...options, folder: bandNoteFolder }); let releaseGroupIds: string[]; try { @@ -548,7 +576,15 @@ export default class MediaDbPlugin extends Plugin { continue; } - await this.createStandardMediaDbNoteFromModel(release, { ...childOptions }); + let songNotesFolder: TFolder | undefined; + if (useTree) { + const albumSeg = this.safeFileTreeSegment(release.title); + songNotesFolder = await this.ensureVaultFolder(normalizePath(`${albumNotesFolder.path}/${albumSeg}`)); + } + + const releaseOpts: CreateNoteOptions = useTree ? { ...childOptions, folder: albumNotesFolder } : { ...childOptions }; + + await this.createStandardMediaDbNoteFromModel(release, releaseOpts); for (const track of release.tracks) { let lyrics = ''; @@ -586,7 +622,9 @@ export default class MediaDbPlugin extends Plugin { userData: { personalRating: 0 }, }); - await this.createStandardMediaDbNoteFromModel(song, { ...childOptions }); + const songOpts: CreateNoteOptions = useTree && songNotesFolder ? { ...childOptions, folder: songNotesFolder } : { ...childOptions }; + + await this.createStandardMediaDbNoteFromModel(song, songOpts); } } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 8ec18191..af72f346 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -113,6 +113,8 @@ export interface MediaDbPluginSettings { musicReleaseFolder: string; bandFolder: string; songFolder: string; + /** When true, band discography import nests albums and songs under bandFolder/BandName/… instead of using album/song import folders. */ + bandUseFileTreeForSongs: boolean; boardgameFolder: string; bookFolder: string; @@ -410,6 +412,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { musicReleaseFolder: 'Media DB/music', bandFolder: 'Media DB/bands', songFolder: 'Media DB/music/songs', + bandUseFileTreeForSongs: false, boardgameFolder: 'Media DB/boardgames', bookFolder: 'Media DB/books', @@ -503,7 +506,11 @@ export class MediaDbSettingTab extends PluginSettingTab { panel: HTMLElement, mediaTypeSetting: MediaTypeMappedSettings, mediaTypeApiMap: Map, - options?: { sectionHeading?: string }, + options?: { + sectionHeading?: string; + hideImportFolder?: boolean; + appendToSection?: (group: SettingGroup) => void; + }, ): void { const mediaType = mediaTypeSetting.mediaType; const descNoun = options?.sectionHeading?.toLowerCase() ?? mediaTypeDisplayName(mediaType).toLowerCase(); @@ -514,27 +521,29 @@ export class MediaDbSettingTab extends PluginSettingTab { const mediaTypeGroup = new SettingGroup(panel); - mediaTypeGroup.addSetting( - setting => - void setting - .setName('Import folder') - .setDesc(`Where newly imported ${descNoun} notes should be placed.`) - .addSearch(cb => { - const suggester = new FolderSuggest(this.app, cb.inputEl); - suggester.onSelect(folder => { - cb.setValue(folder.path); - mediaTypeSetting.setFolder(this.plugin.settings, folder.path); - void this.plugin.saveSettings(); - suggester.close(); - }); - cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) - .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setFolder(this.plugin.settings, data); + if (!options?.hideImportFolder) { + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Import folder') + .setDesc(`Where newly imported ${descNoun} notes should be placed.`) + .addSearch(cb => { + const suggester = new FolderSuggest(this.app, cb.inputEl); + suggester.onSelect(folder => { + cb.setValue(folder.path); + mediaTypeSetting.setFolder(this.plugin.settings, folder.path); void this.plugin.saveSettings(); + suggester.close(); }); - }), - ); + cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) + .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setFolder(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + } mediaTypeGroup.addSetting( setting => @@ -602,18 +611,46 @@ export class MediaDbSettingTab extends PluginSettingTab { } } } + + options?.appendToSection?.(mediaTypeGroup); } private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + const fileTree = this.plugin.settings.bandUseFileTreeForSongs; panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Band), mediaTypeApiMap, { sectionHeading: 'Band' }); + this.renderMediaTypeSection(panel, byType(MediaType.Band), mediaTypeApiMap, { + sectionHeading: 'Band', + appendToSection: group => { + group.addSetting( + setting => + void setting + .setName('Use file trees for songs') + .setDesc( + 'When importing a band, create a subfolder named after the band under the band import folder, place album notes there, and place each album’s songs in a subfolder named after that album. While enabled, album and song import folders are not shown below and are not used for band imports (standalone album/song imports still use those folders).', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.bandUseFileTreeForSongs).onChange(data => { + this.plugin.settings.bandUseFileTreeForSongs = data; + void this.plugin.saveSettings(); + this.display(); + }); + }), + ); + }, + }); panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { sectionHeading: 'Album' }); + this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { + sectionHeading: 'Album', + hideImportFolder: fileTree, + }); panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { sectionHeading: 'Song' }); + this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { + sectionHeading: 'Song', + hideImportFolder: fileTree, + }); } display(): void { From 040a135efd39a5e5edbabe6cf33f6b5cda2a18d6 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:52:11 -0700 Subject: [PATCH 06/60] Merge comic and book tab, fix bug where property mappins were not displayed for Album --- src/main.ts | 6 +-- .../PropertyMappingModelComponent.tsx | 5 ++- src/settings/Settings.ts | 42 ++++++++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2463fb95..58fe034c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,7 +35,7 @@ import { API_SECRET_IDS } from './settings/apiSecretIds'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; -import { getDefaultSettings, MediaDbSettingTab } from './settings/Settings'; +import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOrder } from './settings/Settings'; import { BulkImportHelper } from './utils/BulkImportHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; @@ -882,8 +882,8 @@ export default class MediaDbPlugin extends Plugin { defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), ); - // Store as plain data for serialization - loadedSettings.propertyMappingModels = migratedModels.map(m => m.toJSON()); + // Store as plain data for serialization (canonical order matches settings UI) + loadedSettings.propertyMappingModels = propertyMappingModelsInDisplayOrder(migratedModels.map(m => m.toJSON())); this.settings = loadedSettings; migrateLegacyApiKeysToSecretStorage(this); diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index d9a90173..fd89bdc9 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -1,7 +1,8 @@ import { createSignal, createMemo, For, Show } from 'solid-js'; import { createStore } from 'solid-js/store'; import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; -import { capitalizeFirstLetter } from '../utils/Utils'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; import Icon from './Icon'; interface PropertyMappingModelComponentProps { @@ -36,7 +37,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode return (
-
{capitalizeFirstLetter(modelData.type)}
+
{mediaTypeDisplayName(modelData.type as MediaType)}
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index af72f346..cc929c5c 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -471,6 +471,12 @@ interface MediaDbSettingsTabNavEntry { panel: HTMLElement; } +/** Stable order for property-mapping UI and persisted settings (matches media type settings tabs). */ +export function propertyMappingModelsInDisplayOrder(models: PropertyMappingModelData[]): PropertyMappingModelData[] { + const order = new Map(MEDIA_TYPES.map((t, i) => [t, i])); + return [...models].sort((a, b) => (order.get(a.type) ?? 999) - (order.get(b.type) ?? 999)); +} + // MARK: Settings Tab export class MediaDbSettingTab extends PluginSettingTab { plugin: MediaDbPlugin; @@ -502,6 +508,10 @@ export class MediaDbSettingTab extends PluginSettingTab { private static readonly LEGACY_MUSIC_TAB_IDS: ReadonlySet = new Set(['media-band', 'media-musicRelease', 'media-song']); + private static readonly BOOK_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Book, MediaType.ComicManga]; + + private static readonly LEGACY_BOOK_TAB_IDS: ReadonlySet = new Set(['media-comicManga']); + private renderMediaTypeSection( panel: HTMLElement, mediaTypeSetting: MediaTypeMappedSettings, @@ -653,6 +663,20 @@ export class MediaDbSettingTab extends PluginSettingTab { }); } + private renderBookSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Book), mediaTypeApiMap, { + sectionHeading: 'Book', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.ComicManga), mediaTypeApiMap, { + sectionHeading: 'Comic & Manga', + }); + } + display(): void { const { containerEl } = this; containerEl.empty(); @@ -852,6 +876,7 @@ export class MediaDbSettingTab extends PluginSettingTab { }); let musicTabAdded = false; + let bookTabAdded = false; for (const mediaTypeSetting of mediaTypeSettings) { const mediaType = mediaTypeSetting.mediaType; @@ -865,6 +890,16 @@ export class MediaDbSettingTab extends PluginSettingTab { continue; } + if (MediaDbSettingTab.BOOK_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!bookTabAdded) { + bookTabAdded = true; + addTab('media-book', 'Book', mediaTypeTabIcon(MediaType.Book), panel => { + this.renderBookSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); + } + continue; + } + const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); @@ -890,7 +925,7 @@ export class MediaDbSettingTab extends PluginSettingTab { render( () => PropertyMappingModelsComponent({ - models: structuredClone(this.plugin.settings.propertyMappingModels), + models: propertyMappingModelsInDisplayOrder(structuredClone(this.plugin.settings.propertyMappingModels)), save: (model: PropertyMappingModelData): void => { // Update the matching model in settings (stored as plain data) const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); @@ -898,7 +933,7 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.settings.propertyMappingModels[index] = model; } - new Notice(`MDB: Property mappings for ${model.type} saved successfully.`); + new Notice(`MDB: Property mappings for ${mediaTypeDisplayName(model.type)} saved successfully.`); void this.plugin.saveSettings(); }, }), @@ -913,6 +948,9 @@ export class MediaDbSettingTab extends PluginSettingTab { if (MediaDbSettingTab.LEGACY_MUSIC_TAB_IDS.has(initialId) && validIds.has('media-music')) { initialId = 'media-music'; } + if (MediaDbSettingTab.LEGACY_BOOK_TAB_IDS.has(initialId) && validIds.has('media-book')) { + initialId = 'media-book'; + } if (!validIds.has(initialId)) { initialId = 'general'; } From 64276150e8267d97a21ddeb978020cc00d231a84 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 15:55:40 -0700 Subject: [PATCH 07/60] Move property mappings to each relevant tab / section and make it modal --- src/modals/PropertyMappingModal.ts | 60 +++++++++++++++++++ .../PropertyMappingModelComponent.tsx | 12 +++- .../PropertyMappingModelsComponent.tsx | 16 ----- src/settings/Settings.ts | 58 ++++++------------ src/styles.css | 4 ++ 5 files changed, 91 insertions(+), 59 deletions(-) create mode 100644 src/modals/PropertyMappingModal.ts delete mode 100644 src/settings/PropertyMappingModelsComponent.tsx diff --git a/src/modals/PropertyMappingModal.ts b/src/modals/PropertyMappingModal.ts new file mode 100644 index 00000000..d8c39e75 --- /dev/null +++ b/src/modals/PropertyMappingModal.ts @@ -0,0 +1,60 @@ +import type { App } from 'obsidian'; +import { Modal, Notice } from 'obsidian'; +import { render } from 'solid-js/web'; +import type MediaDbPlugin from '../main'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; +import type { PropertyMappingModelData } from '../settings/PropertyMapping'; +import PropertyMappingModelComponent from '../settings/PropertyMappingModelComponent'; + +export class PropertyMappingModal extends Modal { + private disposeSolid?: () => void; + + constructor( + app: App, + private readonly plugin: MediaDbPlugin, + private readonly mediaType: MediaType, + ) { + super(app); + } + + onOpen(): void { + const { contentEl } = this; + this.setTitle(`Property mappings — ${mediaTypeDisplayName(this.mediaType)}`); + + const modelData = this.plugin.settings.propertyMappingModels.find(m => m.type === this.mediaType); + if (!modelData) { + contentEl.createEl('p', { text: 'No property mapping model found for this media type.' }); + return; + } + + contentEl.createEl('p', { + cls: 'mod-muted', + text: 'Choose whether each metadata field stays as-is, is renamed in front matter, or is omitted. Use Save to persist your changes.', + }); + + const root = contentEl.createDiv(); + this.disposeSolid = render( + () => + PropertyMappingModelComponent({ + model: structuredClone(modelData), + showMediaTypeTitle: false, + save: (model: PropertyMappingModelData): void => { + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + new Notice(`MDB: Property mappings for ${mediaTypeDisplayName(model.type)} saved successfully.`); + void this.plugin.saveSettings(); + }, + }), + root, + ); + } + + onClose(): void { + this.disposeSolid?.(); + this.disposeSolid = undefined; + this.contentEl.empty(); + } +} diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index fd89bdc9..29f00c42 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -8,6 +8,8 @@ import Icon from './Icon'; interface PropertyMappingModelComponentProps { model: PropertyMappingModelData; save: (model: PropertyMappingModelData) => void; + /** When false, hides the media-type heading (e.g. modal title already shows it). Default true. */ + showMediaTypeTitle?: boolean; } export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { @@ -34,10 +36,16 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode } }; + const showTitle = () => props.showMediaTypeTitle !== false; + return (
-
-
{mediaTypeDisplayName(modelData.type as MediaType)}
+
+ +
{mediaTypeDisplayName(modelData.type as MediaType)}
+
diff --git a/src/settings/PropertyMappingModelsComponent.tsx b/src/settings/PropertyMappingModelsComponent.tsx deleted file mode 100644 index 35274ff0..00000000 --- a/src/settings/PropertyMappingModelsComponent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { For } from 'solid-js'; -import { type PropertyMappingModelData } from './PropertyMapping'; -import PropertyMappingModelComponent from './PropertyMappingModelComponent'; - -interface PropertyMappingModelsComponentProps { - models?: PropertyMappingModelData[]; - save: (model: PropertyMappingModelData) => void; -} - -export default function PropertyMappingModelsComponent(props: PropertyMappingModelsComponentProps) { - return ( -
- {model => } -
- ); -} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index cc929c5c..8cb5a771 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,8 +1,8 @@ import type { App, IconName } from 'obsidian'; -import { Notice, Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; -import { render } from 'solid-js/web'; +import { Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; +import { PropertyMappingModal } from '../modals/PropertyMappingModal'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; @@ -10,7 +10,6 @@ import type { ApiSecretId } from './apiSecretIds'; import { API_SECRET_IDS } from './apiSecretIds'; import type { PropertyMappingModelData } from './PropertyMapping'; import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; -import PropertyMappingModelsComponent from './PropertyMappingModelsComponent'; import { FileSuggest } from './suggesters/FileSuggest'; import { FolderSuggest } from './suggesters/FolderSuggest'; @@ -623,6 +622,20 @@ export class MediaDbSettingTab extends PluginSettingTab { } options?.appendToSection?.(mediaTypeGroup); + + if (this.plugin.settings.useDefaultFrontMatter) { + mediaTypeGroup.addSetting(setting => + void setting + .setName('Property mappings') + .setDesc(`How metadata fields map to frontmatter for ${descNoun} notes.`) + .addButton(btn => { + btn.setButtonText('Edit'); + btn.onClick(() => { + new PropertyMappingModal(this.app, this.plugin, mediaType).open(); + }); + }), + ); + } } private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { @@ -639,7 +652,7 @@ export class MediaDbSettingTab extends PluginSettingTab { void setting .setName('Use file trees for songs') .setDesc( - 'When importing a band, create a subfolder named after the band under the band import folder, place album notes there, and place each album’s songs in a subfolder named after that album. While enabled, album and song import folders are not shown below and are not used for band imports (standalone album/song imports still use those folders).', + 'Use a file tree hierarchy to store albums and songs for each band.', ) .addToggle(cb => { cb.setValue(this.plugin.settings.bandUseFileTreeForSongs).onChange(data => { @@ -906,43 +919,6 @@ export class MediaDbSettingTab extends PluginSettingTab { }); } - // MARK: Property mappings - - if (this.plugin.settings.useDefaultFrontMatter) { - addTab('property-mappings', 'Property mappings', 'list-tree', panel => { - const mappingGroup = new SettingGroup(panel); - mappingGroup.addSetting(setting => { - setting - .setName('Property mappings explanation') - .setDesc( - fragWithHTML( - '

Here you can customize how metadata fields are mapped to property names in the front matter of the created notes.

' + - '

You can choose to keep the original name, rename the property, or remove it entirely.

' + - '

Remember to save your changes using the save button for each individual category.

', - ), - ); - - render( - () => - PropertyMappingModelsComponent({ - models: propertyMappingModelsInDisplayOrder(structuredClone(this.plugin.settings.propertyMappingModels)), - save: (model: PropertyMappingModelData): void => { - // Update the matching model in settings (stored as plain data) - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - - new Notice(`MDB: Property mappings for ${mediaTypeDisplayName(model.type)} saved successfully.`); - void this.plugin.saveSettings(); - }, - }), - setting.descEl, - ); - }); - }); - } - const validIds = new Set(tabEntries.map(t => t.id)); let initialId = this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; if (MediaDbSettingTab.LEGACY_MUSIC_TAB_IDS.has(initialId) && validIds.has('media-music')) { diff --git a/src/styles.css b/src/styles.css index f605a85c..c9c5edbb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -101,6 +101,10 @@ small.media-db-plugin-list-text { gap: var(--size-4-3); } +.media-db-plugin-property-mappings-model-header--actions-only { + justify-content: flex-end; +} + .media-db-plugin-property-mappings-model-header .setting-item-name { font-weight: var(--font-semibold); font-size: var(--font-ui-medium); From eec6d65c465a9609158640d4013c180fa5616bcd Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:01:00 -0700 Subject: [PATCH 08/60] merge movie tabs --- src/settings/Settings.ts | 43 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 8cb5a771..c8e28c36 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -470,7 +470,7 @@ interface MediaDbSettingsTabNavEntry { panel: HTMLElement; } -/** Stable order for property-mapping UI and persisted settings (matches media type settings tabs). */ +/** Stable order for property-mapping UI and persisted settings (`MEDIA_TYPES`; settings tabs move Board game last). */ export function propertyMappingModelsInDisplayOrder(models: PropertyMappingModelData[]): PropertyMappingModelData[] { const order = new Map(MEDIA_TYPES.map((t, i) => [t, i])); return [...models].sort((a, b) => (order.get(a.type) ?? 999) - (order.get(b.type) ?? 999)); @@ -511,6 +511,10 @@ export class MediaDbSettingTab extends PluginSettingTab { private static readonly LEGACY_BOOK_TAB_IDS: ReadonlySet = new Set(['media-comicManga']); + private static readonly VIDEO_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Season]; + + private static readonly LEGACY_VIDEO_TAB_IDS: ReadonlySet = new Set(['media-series', 'media-season']); + private renderMediaTypeSection( panel: HTMLElement, mediaTypeSetting: MediaTypeMappedSettings, @@ -690,6 +694,24 @@ export class MediaDbSettingTab extends PluginSettingTab { }); } + private renderVideoSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Movie), mediaTypeApiMap, { + sectionHeading: 'Movie', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Series), mediaTypeApiMap, { + sectionHeading: 'Series', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Season), mediaTypeApiMap, { + sectionHeading: 'Season', + }); + } + display(): void { const { containerEl } = this; containerEl.empty(); @@ -720,7 +742,10 @@ export class MediaDbSettingTab extends PluginSettingTab { nav.addEventListener('click', () => selectTab(id)); }; - const mediaTypeSettings = MEDIA_TYPES.map(mt => new MediaTypeMappedSettings(mt)); + const mediaTypeSettings = [ + ...MEDIA_TYPES.filter(mt => mt !== MediaType.BoardGame).map(mt => new MediaTypeMappedSettings(mt)), + new MediaTypeMappedSettings(MediaType.BoardGame), + ]; const mediaTypeApiMap = new Map(); for (const api of this.plugin.apiManager.apis) { @@ -890,6 +915,7 @@ export class MediaDbSettingTab extends PluginSettingTab { let musicTabAdded = false; let bookTabAdded = false; + let videoTabAdded = false; for (const mediaTypeSetting of mediaTypeSettings) { const mediaType = mediaTypeSetting.mediaType; @@ -913,6 +939,16 @@ export class MediaDbSettingTab extends PluginSettingTab { continue; } + if (MediaDbSettingTab.VIDEO_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!videoTabAdded) { + videoTabAdded = true; + addTab('media-movie', 'Movie', mediaTypeTabIcon(MediaType.Movie), panel => { + this.renderVideoSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); + } + continue; + } + const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); @@ -927,6 +963,9 @@ export class MediaDbSettingTab extends PluginSettingTab { if (MediaDbSettingTab.LEGACY_BOOK_TAB_IDS.has(initialId) && validIds.has('media-book')) { initialId = 'media-book'; } + if (MediaDbSettingTab.LEGACY_VIDEO_TAB_IDS.has(initialId) && validIds.has('media-movie')) { + initialId = 'media-movie'; + } if (!validIds.has(initialId)) { initialId = 'general'; } From 43a94fd5686c8d79305a69c263e6aa98be7527e8 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:04:18 -0700 Subject: [PATCH 09/60] make type, id and dataSource properties remappable --- src/settings/PropertyMapper.ts | 46 +++++++++++++++++++-------------- src/settings/PropertyMapping.ts | 14 ++++++++++ src/settings/Settings.ts | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index bf08d32b..0f2d1f42 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,4 +1,3 @@ -import type { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { PropertyMappingOption } from './PropertyMapping'; @@ -72,40 +71,49 @@ export class PropertyMapper { * @param obj */ convertObjectBack(obj: Record): Record { - if (!Object.hasOwn(obj, 'type')) { - return obj; + const models = this.plugin.settings.propertyMappingModels; + + let matchedModel: (typeof models)[number] | undefined; + for (const model of models) { + const typePm = model.properties.find(p => p.property === 'type'); + const typeKey = + typePm?.mapping === PropertyMappingOption.Map && typePm.newProperty + ? typePm.newProperty + : 'type'; + if (!Object.hasOwn(obj, typeKey)) { + continue; + } + let typeVal: unknown = obj[typeKey]; + if (typeVal === 'manga') { + typeVal = 'comicManga'; + console.debug(`MDB | updated metadata type`, typeVal); + } + if (typeVal === model.type) { + matchedModel = model; + break; + } } - if (obj.type === 'manga') { - obj.type = 'comicManga'; - console.debug(`MDB | updated metadata type`, obj.type); - } - if (MEDIA_TYPES.contains(obj.type as MediaType)) { + if (!matchedModel) { return obj; } - const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type); - const propertyMappings = propertyMappingModel?.properties ?? []; - + const propertyMappings = matchedModel.properties; const originalObj: Record = {}; objLoop: for (const [key, value] of Object.entries(obj)) { - // first try if it is a normal property for (const propertyMapping of propertyMappings) { if (propertyMapping.property === key) { - // @ts-ignore originalObj[key] = value; - continue objLoop; } } - - // otherwise see if it is a mapped property for (const propertyMapping of propertyMappings) { - if (propertyMapping.newProperty === key) { - // @ts-ignore + if ( + propertyMapping.mapping === PropertyMappingOption.Map && + propertyMapping.newProperty === key + ) { originalObj[propertyMapping.property] = value; - continue objLoop; } } diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index 82d044ea..9359f547 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -23,6 +23,8 @@ export enum PropertyMappingOption { export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; +const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id', 'dataSource'] as const; + export class PropertyMappingModel { type: MediaType; properties: PropertyMapping[]; @@ -194,6 +196,18 @@ export class PropertyMapping { } } + if ( + (METADATA_KEYS_REQUIRED_IN_NOTE as readonly string[]).includes(this.property) && + this.mapping === PropertyMappingOption.Remove + ) { + return { + res: false, + err: new PropertyMappingValidationError( + `Error in property mapping "${this.toString()}": type, id, and dataSource must appear in the note (you can remap them, but not remove them).`, + ), + }; + } + if (this.mapping === PropertyMappingOption.Default) { return { res: true }; } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c8e28c36..95b4bf8f 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -431,7 +431,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { bookPropertyConversionRules: '', }; -export const lockedPropertyMappings: string[] = ['type', 'id', 'dataSource']; +export const lockedPropertyMappings: string[] = []; export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings { const defaultSettings = DEFAULT_SETTINGS; From 5da2dfa70d2464016053a01410dde4d684c01eeb Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:29:50 -0700 Subject: [PATCH 10/60] Make dataSource removable for music media --- src/api/APIManager.ts | 39 +++++++++++++++++++++++++++--- src/api/apis/MusicBrainzAPI.ts | 5 ++-- src/api/apis/MusicBrainzBandAPI.ts | 5 ++-- src/api/musicBrainzConstants.ts | 19 +++++++++++++++ src/main.ts | 27 +++++++++++++++++---- src/settings/PropertyMapping.ts | 15 ++++++++++-- 6 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 src/api/musicBrainzConstants.ts diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 741c50e4..0fdf8a07 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -1,5 +1,11 @@ import { Notice } from 'obsidian'; import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from '../utils/MediaType'; +import { + isMusicBrainzFamilyDataSource, + musicBrainzRegisteredApiName, + MUSICBRAINZ_NOTE_DATA_SOURCE, +} from './musicBrainzConstants'; import type { APIModel } from './APIModel'; export class APIManager { @@ -40,18 +46,43 @@ export class APIManager { * @param item */ async queryDetailedInfo(item: MediaTypeModel): Promise { - 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 Band 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 { + async queryDetailedInfoById(id: string, apiName: string, mediaType?: MediaType): Promise { + const trimmed = apiName.trim(); + const effectiveApiName = + trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) + ? MUSICBRAINZ_NOTE_DATA_SOURCE + : trimmed || apiName; + + if (isMusicBrainzFamilyDataSource(effectiveApiName) && mediaType !== undefined) { + const registeredName = musicBrainzRegisteredApiName(mediaType); + if (registeredName) { + const api = this.getApiByName(registeredName); + if (api) { + try { + return await api.getById(id); + } catch (e) { + new Notice(`Error querying ${api.apiName}: ${e}`); + console.warn(e); + + return undefined; + } + } + } + } + for (const api of this.apis) { - if (api.apiName === apiName) { + if (api.apiName === effectiveApiName) { try { return api.getById(id); } catch (e) { diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index e1d0d7a8..a70727fe 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -4,6 +4,7 @@ import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MusicReleaseModel } from '../../models/MusicReleaseModel'; import { MediaType } from '../../utils/MediaType'; import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; import { APIModel } from '../APIModel'; // sadly no open api schema available @@ -135,7 +136,7 @@ export class MusicBrainzAPI extends APIModel { englishTitle: result.title, year: new Date(result['first-release-date']).getFullYear().toString(), releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/release-group/' + result.id, id: result.id, image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', @@ -207,7 +208,7 @@ export class MusicBrainzAPI extends APIModel { englishTitle: result.title, year: new Date(result['first-release-date']).getFullYear().toString(), releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: this.apiName, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/release-group/' + result.id, id: result.id, image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzBandAPI.ts index 94301aac..fd1167c2 100644 --- a/src/api/apis/MusicBrainzBandAPI.ts +++ b/src/api/apis/MusicBrainzBandAPI.ts @@ -4,6 +4,7 @@ import { BandModel } from '../../models/BandModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; import { APIModel } from '../APIModel'; interface ArtistTag { @@ -115,7 +116,7 @@ export class MusicBrainzBandAPI extends APIModel { year: begin ? (begin.split('-')[0] ?? '') : '', beginYear: begin ? (begin.split('-')[0] ?? '') : '', releaseDate: '', - dataSource: this.apiName, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/artist/' + artist.id, id: artist.id, country: artist.country ?? '', @@ -163,7 +164,7 @@ export class MusicBrainzBandAPI extends APIModel { year: beginYear, beginYear, releaseDate: begin ? (this.plugin.dateFormatter.format(begin, this.apiDateFormat) ?? 'unknown') : '', - dataSource: this.apiName, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/artist/' + artist.id, id: artist.id, country: artist.country ?? '', diff --git a/src/api/musicBrainzConstants.ts b/src/api/musicBrainzConstants.ts new file mode 100644 index 00000000..6ad4c27f --- /dev/null +++ b/src/api/musicBrainzConstants.ts @@ -0,0 +1,19 @@ +import { MediaType } from '../utils/MediaType'; + +/** Stored on notes for any row backed by MusicBrainz (release, band, or song). */ +export const MUSICBRAINZ_NOTE_DATA_SOURCE = 'MusicBrainz'; + +export function isMusicBrainzFamilyDataSource(dataSource: string): boolean { + return dataSource.contains('MusicBrainz'); +} + +/** Which registered API implements getById for this media type. */ +export function musicBrainzRegisteredApiName(mediaType: MediaType): 'MusicBrainz API' | 'MusicBrainz Band API' | undefined { + if (mediaType === MediaType.Band) { + return 'MusicBrainz Band API'; + } + if (mediaType === MediaType.MusicRelease || mediaType === MediaType.Song) { + return 'MusicBrainz API'; + } + return undefined; +} diff --git a/src/main.ts b/src/main.ts index 58fe034c..f0ad4daa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from import { requestUrl, normalizePath } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './api/musicBrainzConstants'; import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { ComicVineAPI } from './api/apis/ComicVineAPI'; import { GiantBombAPI } from './api/apis/GiantBombAPI'; @@ -605,7 +606,7 @@ export default class MediaDbPlugin extends Plugin { englishTitle: track.title, year: release.year, releaseDate: release.releaseDate, - dataSource: genius.isConfigured() ? 'MusicBrainz / Genius' : 'MusicBrainz', + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: geniusUrl || release.url, id: `${release.id}-t${track.number}`, image: release.image, @@ -833,7 +834,7 @@ export default class MediaDbPlugin extends Plugin { /** * Update the active note by querying the API again. - * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. + * Tries to read the type and id of the active note (and dataSource when required). If successful it will query the api, delete the old note and create a new one. */ async updateActiveNote(onlyMetadata: boolean = false): Promise { const activeFile = this.app.workspace.getActiveFile() ?? undefined; @@ -846,17 +847,33 @@ export default class MediaDbPlugin extends Plugin { console.debug(`MDB | read metadata`, metadata); - if (!metadata?.type || !metadata?.dataSource || !metadata?.id) { + if (!metadata?.type || !metadata?.id) { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); } - const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; + const mediaType = metadata.type as MediaType; + let dataSource = typeof metadata.dataSource === 'string' ? metadata.dataSource.trim() : ''; + if ( + !dataSource && + musicBrainzRegisteredApiName(mediaType) + ) { + dataSource = MUSICBRAINZ_NOTE_DATA_SOURCE; + } + if (!dataSource) { + throw new Error('MDB | active note is missing dataSource (required for this media type)'); + } + + const validOldMetadata: MediaTypeModelObj = { ...metadata, dataSource } as unknown as MediaTypeModelObj; console.debug(`MDB | validOldMetadata`, validOldMetadata); const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); + let newMediaTypeModel = await this.apiManager.queryDetailedInfoById( + validOldMetadata.id, + validOldMetadata.dataSource, + validOldMetadata.type as MediaType, + ); if (!newMediaTypeModel) { return; } diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index 9359f547..c23ddf3c 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,3 +1,4 @@ +import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; import type { MediaType } from '../utils/MediaType'; import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; @@ -23,7 +24,7 @@ export enum PropertyMappingOption { export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; -const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id', 'dataSource'] as const; +const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id'] as const; export class PropertyMappingModel { type: MediaType; @@ -79,6 +80,16 @@ export class PropertyMappingModel { } } + const dataSourceRule = this.properties.find(p => p.property === 'dataSource'); + if (dataSourceRule?.mapping === PropertyMappingOption.Remove && !musicBrainzRegisteredApiName(this.type)) { + return { + res: false, + err: new PropertyMappingValidationError( + `Removing dataSource is only allowed for band, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, + ), + }; + } + return { res: true, }; @@ -203,7 +214,7 @@ export class PropertyMapping { return { res: false, err: new PropertyMappingValidationError( - `Error in property mapping "${this.toString()}": type, id, and dataSource must appear in the note (you can remap them, but not remove them).`, + `Error in property mapping "${this.toString()}": type and id must appear in the note (you can remap them, but not remove them).`, ), }; } From 4aa2e47fbd93d19aac797afb50ac386f707471fe Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:30:00 -0700 Subject: [PATCH 11/60] Make Type metadata adjustable --- src/main.ts | 17 +++++---- src/settings/PropertyMapper.ts | 10 +++--- src/settings/Settings.ts | 57 ++++++++++++++++++++++++++++++ src/utils/noteTypeSettings.ts | 63 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 src/utils/noteTypeSettings.ts diff --git a/src/main.ts b/src/main.ts index f0ad4daa..a89b3b3e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,6 +40,7 @@ import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOr import { BulkImportHelper } from './utils/BulkImportHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; +import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; import type { CreateNoteOptions } from './utils/Utils'; @@ -671,6 +672,7 @@ export default class MediaDbPlugin extends Plugin { } generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { + mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); return stringifyYaml(fileMetadata); } @@ -682,6 +684,8 @@ export default class MediaDbPlugin extends Plugin { * @param options */ async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); + let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); let fileMetadata: Record; @@ -851,7 +855,10 @@ export default class MediaDbPlugin extends Plugin { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); } - const mediaType = metadata.type as MediaType; + const mediaType = resolveMetadataTypeToMediaType(this.settings, metadata.type); + if (mediaType === undefined) { + throw new Error('MDB | active note type is not recognized; check Settings → Note type for each media kind'); + } let dataSource = typeof metadata.dataSource === 'string' ? metadata.dataSource.trim() : ''; if ( !dataSource && @@ -866,14 +873,10 @@ export default class MediaDbPlugin extends Plugin { const validOldMetadata: MediaTypeModelObj = { ...metadata, dataSource } as unknown as MediaTypeModelObj; console.debug(`MDB | validOldMetadata`, validOldMetadata); - const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); + const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, mediaType); console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById( - validOldMetadata.id, - validOldMetadata.dataSource, - validOldMetadata.type as MediaType, - ); + let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource, mediaType); if (!newMediaTypeModel) { return; } diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 0f2d1f42..27e58712 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,5 +1,5 @@ import type MediaDbPlugin from '../main'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; +import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { @@ -22,11 +22,12 @@ export class PropertyMapper { // console.log(obj.type); - if (MEDIA_TYPES.filter(x => x.toString() == obj.type).length < 1) { + const internalMediaType = resolveMetadataTypeToMediaType(this.plugin.settings, obj.type); + if (!internalMediaType) { return obj; } - const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type); + const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === internalMediaType); if (!propertyMappingModel) { return obj; } @@ -88,7 +89,8 @@ export class PropertyMapper { typeVal = 'comicManga'; console.debug(`MDB | updated metadata type`, typeVal); } - if (typeVal === model.type) { + const typeStr = String(typeVal).trim(); + if (typeStr === model.type || typeStr === noteTypeValueForMedia(this.plugin.settings, model.type)) { matchedModel = model; break; } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 95b4bf8f..d8db7fc4 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -5,6 +5,7 @@ import type MediaDbPlugin from '../main'; import { PropertyMappingModal } from '../modals/PropertyMappingModal'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; +import { noteTypeValueForMedia, setNoteTypeForMedia } from '../utils/noteTypeSettings'; import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; import type { ApiSecretId } from './apiSecretIds'; import { API_SECRET_IDS } from './apiSecretIds'; @@ -112,6 +113,19 @@ export interface MediaDbPluginSettings { musicReleaseFolder: string; bandFolder: string; songFolder: string; + + /** Frontmatter `type` for each media kind (empty = default internal id, e.g. movie, musicRelease). */ + movieNoteType: string; + seriesNoteType: string; + seasonNoteType: string; + mangaNoteType: string; + gameNoteType: string; + wikiNoteType: string; + musicReleaseNoteType: string; + bandNoteType: string; + songNoteType: string; + boardgameNoteType: string; + bookNoteType: string; /** When true, band discography import nests albums and songs under bandFolder/BandName/… instead of using album/song import folders. */ bandUseFileTreeForSongs: boolean; boardgameFolder: string; @@ -337,6 +351,20 @@ class MediaTypeMappedSettings { break; } } + + getNoteType(settings: MediaDbPluginSettings): string { + const configured = noteTypeValueForMedia(settings, this.mediaType); + return configured === this.mediaType ? '' : configured; + } + + setNoteType(settings: MediaDbPluginSettings, value: string): void { + const trimmed = value.trim(); + if (trimmed === '' || trimmed === this.mediaType) { + setNoteTypeForMedia(settings, this.mediaType, ''); + return; + } + setNoteTypeForMedia(settings, this.mediaType, value); + } } // MARK: Defaults @@ -415,6 +443,18 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { boardgameFolder: 'Media DB/boardgames', bookFolder: 'Media DB/books', + movieNoteType: '', + seriesNoteType: '', + seasonNoteType: '', + mangaNoteType: '', + gameNoteType: '', + wikiNoteType: '', + musicReleaseNoteType: '', + bandNoteType: '', + songNoteType: '', + boardgameNoteType: '', + bookNoteType: '', + propertyMappingModels: [], // DEPRECATED @@ -558,6 +598,23 @@ export class MediaDbSettingTab extends PluginSettingTab { ); } + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Note type') + .setDesc( + `Value for the "type" field in frontmatter. Leave blank to use the default (${mediaType}).`, + ) + .addText(cb => { + cb.setPlaceholder(String(mediaType)) + .setValue(mediaTypeSetting.getNoteType(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setNoteType(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + mediaTypeGroup.addSetting( setting => void setting diff --git a/src/utils/noteTypeSettings.ts b/src/utils/noteTypeSettings.ts new file mode 100644 index 00000000..521f94ad --- /dev/null +++ b/src/utils/noteTypeSettings.ts @@ -0,0 +1,63 @@ +import type { MediaDbPluginSettings } from '../settings/Settings'; +import { MEDIA_TYPES } from './MediaTypeManager'; +import { MediaType } from './MediaType'; + +const MEDIA_TYPE_TO_NOTE_TYPE_KEY: Record = { + [MediaType.Band]: 'bandNoteType', + [MediaType.BoardGame]: 'boardgameNoteType', + [MediaType.Book]: 'bookNoteType', + [MediaType.ComicManga]: 'mangaNoteType', + [MediaType.Game]: 'gameNoteType', + [MediaType.Movie]: 'movieNoteType', + [MediaType.MusicRelease]: 'musicReleaseNoteType', + [MediaType.Season]: 'seasonNoteType', + [MediaType.Series]: 'seriesNoteType', + [MediaType.Song]: 'songNoteType', + [MediaType.Wiki]: 'wikiNoteType', +}; + +/** + * Value written to frontmatter `type` for this media kind. Falls back to the internal + * {@link MediaType} string when the setting is empty. + */ +export function noteTypeValueForMedia(settings: MediaDbPluginSettings, mediaType: MediaType): string { + const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; + const raw = settings[key]; + const s = typeof raw === 'string' ? raw.trim() : ''; + return s !== '' ? s : mediaType; +} + +export function setNoteTypeForMedia(settings: MediaDbPluginSettings, mediaType: MediaType, value: string): void { + const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; + (settings as unknown as Record)[key as string] = value; +} + +/** + * Maps a frontmatter `type` string (legacy enum id or configured custom string) to {@link MediaType}. + */ +export function resolveMetadataTypeToMediaType( + settings: MediaDbPluginSettings, + noteType: unknown, +): MediaType | undefined { + if (noteType === undefined || noteType === null) { + return undefined; + } + let s = String(noteType).trim(); + if (s === '') { + return undefined; + } + if (s === 'manga') { + s = MediaType.ComicManga; + } + for (const mt of MEDIA_TYPES) { + if (mt === s) { + return mt; + } + } + for (const mt of MEDIA_TYPES) { + if (noteTypeValueForMedia(settings, mt) === s) { + return mt; + } + } + return undefined; +} From 33a8719bb0959904bce5f34d837911f9bb774800 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:32:22 -0700 Subject: [PATCH 12/60] Make year number --- src/api/apis/BoardGameGeekAPI.ts | 5 +++-- src/api/apis/ComicVineAPI.ts | 5 +++-- src/api/apis/GiantBombAPI.ts | 10 +++++----- src/api/apis/IGDBAPI.ts | 9 ++++++--- src/api/apis/MALAPI.ts | 6 +++--- src/api/apis/MALAPIManga.ts | 6 +++--- src/api/apis/MobyGamesAPI.ts | 4 ++-- src/api/apis/MusicBrainzAPI.ts | 10 +++++++--- src/api/apis/MusicBrainzBandAPI.ts | 6 +++--- src/api/apis/OMDbAPI.ts | 13 +++++++------ src/api/apis/OpenLibraryAPI.ts | 4 ++-- src/api/apis/RAWGAPI.ts | 4 ++-- src/api/apis/SteamAPI.ts | 6 +++--- src/api/apis/TMDBMovieAPI.ts | 4 ++-- src/api/apis/TMDBSeasonAPI.ts | 6 +++--- src/api/apis/TMDBSeriesAPI.ts | 4 ++-- src/api/apis/VNDBAPI.ts | 7 +++++-- src/api/apis/WikipediaAPI.ts | 2 +- src/main.ts | 2 +- src/models/MediaTypeModel.ts | 4 ++-- src/utils/Utils.ts | 30 +++++++++++++++++++++++++++++- 21 files changed, 94 insertions(+), 53 deletions(-) diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 31973c42..8c4929a3 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -2,6 +2,7 @@ import { requestUrl } from 'obsidian'; import { BoardGameModel } from 'src/models/BoardGameModel'; import type MediaDbPlugin from '../../main'; import { apiSecrets } from '../../settings/apiSecretHelpers'; +import { coerceYear } from '../../utils/Utils'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -63,7 +64,7 @@ export class BoardGameGeekAPI extends APIModel { id, title, englishTitle: title, - year, + year: coerceYear(year), }), ); } @@ -122,7 +123,7 @@ export class BoardGameGeekAPI extends APIModel { return new BoardGameModel({ title: title ?? undefined, englishTitle: title ?? undefined, - year: year === '0' ? '' : year, + year: year === '0' ? 0 : coerceYear(year), dataSource: this.apiName, url: `https://boardgamegeek.com/boardgame/${id}`, id: id, diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index 555b122e..bcc48cf9 100644 --- a/src/api/apis/ComicVineAPI.ts +++ b/src/api/apis/ComicVineAPI.ts @@ -6,6 +6,7 @@ import type MediaDbPlugin from '../../main'; import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; // sadly no open api schema available @@ -48,7 +49,7 @@ export class ComicVineAPI extends APIModel { new ComicMangaModel({ title: result.name, englishTitle: result.name, - year: result.start_year, + year: coerceYear(result.start_year), dataSource: this.apiName, id: `4050-${result.id}`, publishers: result.publisher?.name, @@ -93,7 +94,7 @@ export class ComicVineAPI extends APIModel { englishTitle: result.name, alternateTitles: result.aliases, plot: result.deck, - year: result.start_year, + year: coerceYear(result.start_year), dataSource: this.apiName, url: result.site_detail_url, id: `4050-${result.id}`, diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 26a4a36e..36185f1d 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -1,5 +1,5 @@ import createClient from 'openapi-fetch'; -import { obsidianFetch } from 'src/utils/Utils'; +import { coerceYear, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import { apiSecrets } from '../../settings/apiSecretHelpers'; import { GameModel } from '../../models/GameModel'; @@ -56,13 +56,13 @@ export class GiantBombAPI extends APIModel { const ret: MediaTypeModel[] = []; for (const result of data ?? []) { - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; + const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; ret.push( new GameModel({ title: result.name, englishTitle: result.name, - year: year, + year: coerceYear(year), dataSource: this.apiName, id: result.guid?.toString(), }), @@ -112,7 +112,7 @@ export class GiantBombAPI extends APIModel { console.log(result); // sadly the only OpenAPI definition I could find doesn't have the right types - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear().toString() : undefined; + const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; const developers = result.developers as | { name: string; @@ -140,7 +140,7 @@ export class GiantBombAPI extends APIModel { type: MediaType.Game, title: result.name, englishTitle: result.name, - year: year, + year: coerceYear(year), dataSource: this.apiName, url: result.site_detail_url, id: result.guid?.toString(), diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 8b717e36..ea05a9b0 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -4,6 +4,7 @@ import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { apiSecrets } from '../../settings/apiSecretHelpers'; import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; interface IGDBCover { url: string; } @@ -67,10 +68,10 @@ export class IGDBAPI extends APIModel { const data = response.json as IGDBGame[]; return data.map(result => { - const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : ''; + const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0; const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, year: year, + type: MediaType.Game, title: result.name, englishTitle: result.name, year: coerceYear(year), dataSource: this.apiName, id: result.id.toString(), image: image }); }); @@ -103,7 +104,9 @@ export class IGDBAPI extends APIModel { return new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name, - year: result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear().toString() : '', + year: coerceYear( + result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0, + ), dataSource: this.apiName, url: result.url, id: result.id.toString(), developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], onlineRating: result.total_rating, image: image, released: true, diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index 8d0e34b6..cfb4acea 100644 --- a/src/api/apis/MALAPI.ts +++ b/src/api/apis/MALAPI.ts @@ -1,5 +1,5 @@ import createClient from 'openapi-fetch'; -import { isTruthy, obsidianFetch } from 'src/utils/Utils'; +import { coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; @@ -55,7 +55,7 @@ export class MALAPI extends APIModel { for (const result of data ?? []) { const resType = result.type?.toLowerCase(); const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString() ?? ''; + const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); const id = result.mal_id?.toString(); if (type === undefined) { @@ -124,7 +124,7 @@ export class MALAPI extends APIModel { const resType = result.type?.toLowerCase(); const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.year?.toString() ?? result.aired?.prop?.from?.year?.toString(); + const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); const new_id = result.mal_id?.toString(); if (type === undefined) { diff --git a/src/api/apis/MALAPIManga.ts b/src/api/apis/MALAPIManga.ts index 83d47322..4293ca1a 100644 --- a/src/api/apis/MALAPIManga.ts +++ b/src/api/apis/MALAPIManga.ts @@ -1,5 +1,5 @@ import createClient from 'openapi-fetch'; -import { isTruthy, obsidianFetch } from 'src/utils/Utils'; +import { coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import { ComicMangaModel } from '../../models/ComicMangaModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; @@ -57,7 +57,7 @@ export class MALAPIManga extends APIModel { for (const result of data ?? []) { const resType = result.type?.toLowerCase(); const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.published?.prop?.from?.year?.toString() ?? ''; + const year = coerceYear(result.published?.prop?.from?.year); const id = result.mal_id?.toString(); ret.push( @@ -122,7 +122,7 @@ export class MALAPIManga extends APIModel { const resType = result.type?.toLowerCase(); const type = resType ? this.typeMappings.get(resType) : undefined; - const year = result.published?.prop?.from?.year?.toString() ?? ''; + const year = coerceYear(result.published?.prop?.from?.year); const new_id = result.mal_id?.toString(); return new ComicMangaModel({ diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index 0b8a6086..2e9ab1d6 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -59,7 +59,7 @@ export class MobyGamesAPI extends APIModel { type: MediaType.Game, title: result.title, englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), + year: new Date(result.platforms[0].first_release_date).getFullYear(), dataSource: this.apiName, id: result.game_id, }), @@ -94,7 +94,7 @@ export class MobyGamesAPI extends APIModel { type: MediaType.Game, title: result.title, englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear().toString(), + year: new Date(result.platforms[0].first_release_date).getFullYear(), dataSource: this.apiName, url: `https://www.mobygames.com/game/${result.game_id}`, id: result.game_id, diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index a70727fe..1d3abfdc 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -3,7 +3,7 @@ import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MusicReleaseModel } from '../../models/MusicReleaseModel'; import { MediaType } from '../../utils/MediaType'; -import { contactEmail, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { contactEmail, coerceYear, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; import { APIModel } from '../APIModel'; @@ -134,7 +134,9 @@ export class MusicBrainzAPI extends APIModel { type: 'musicRelease', title: result.title, englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), + year: coerceYear( + result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, + ), releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/release-group/' + result.id, @@ -206,7 +208,9 @@ export class MusicBrainzAPI extends APIModel { type: 'musicRelease', title: result.title, englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), + year: coerceYear( + result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, + ), releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, url: 'https://musicbrainz.org/release-group/' + result.id, diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzBandAPI.ts index fd1167c2..eb936873 100644 --- a/src/api/apis/MusicBrainzBandAPI.ts +++ b/src/api/apis/MusicBrainzBandAPI.ts @@ -3,7 +3,7 @@ import type MediaDbPlugin from '../../main'; import { BandModel } from '../../models/BandModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; -import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { coerceYear, contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; import { APIModel } from '../APIModel'; @@ -113,7 +113,7 @@ export class MusicBrainzBandAPI extends APIModel { type: 'band', title: artist.name, englishTitle: artist.name, - year: begin ? (begin.split('-')[0] ?? '') : '', + year: coerceYear(begin ? (begin.split('-')[0] ?? '') : ''), beginYear: begin ? (begin.split('-')[0] ?? '') : '', releaseDate: '', dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, @@ -161,7 +161,7 @@ export class MusicBrainzBandAPI extends APIModel { type: 'band', title: artist.name, englishTitle: artist.name, - year: beginYear, + year: coerceYear(beginYear), beginYear, releaseDate: begin ? (this.plugin.dateFormatter.format(begin, this.apiDateFormat) ?? 'unknown') : '', dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 362fb7ad..39a35acd 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -6,6 +6,7 @@ import { MovieModel } from '../../models/MovieModel'; import { SeriesModel } from '../../models/SeriesModel'; import { apiSecrets } from '../../settings/apiSecretHelpers'; import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; interface ErrorResponse { @@ -127,7 +128,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, id: result.imdbID, }), @@ -138,7 +139,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, id: result.imdbID, }), @@ -149,7 +150,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, id: result.imdbID, }), @@ -200,7 +201,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, url: `https://www.imdb.com/title/${result.imdbID}/`, id: result.imdbID, @@ -231,7 +232,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, url: `https://www.imdb.com/title/${result.imdbID}/`, id: result.imdbID, @@ -262,7 +263,7 @@ export class OMDbAPI extends APIModel { type: type, title: result.Title, englishTitle: result.Title, - year: result.Year, + year: coerceYear(result.Year), dataSource: this.apiName, url: `https://www.imdb.com/title/${result.imdbID}/`, id: result.imdbID, diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts index 645d15ba..ae9b74ed 100644 --- a/src/api/apis/OpenLibraryAPI.ts +++ b/src/api/apis/OpenLibraryAPI.ts @@ -74,7 +74,7 @@ export class OpenLibraryAPI extends APIModel { new BookModel({ title: result.title, englishTitle: result.title, - year: result.first_publish_year?.toString() ?? 'unknown', + year: result.first_publish_year ?? 0, dataSource: this.apiName, id: result.key, author: result.author_name?.join(', '), @@ -132,7 +132,7 @@ export class OpenLibraryAPI extends APIModel { return new BookModel({ title: title, - year: result.first_publish_year?.toString() ?? 'unknown', + year: result.first_publish_year ?? 0, dataSource: this.apiName, url: `https://openlibrary.org` + key, id: key, diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index 8143fc09..cdff1690 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -37,7 +37,7 @@ export class RAWGAPI extends APIModel { const data = response.json as RAWGSearchResponse; return data.results.map(result => new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', + year: result.released ? new Date(result.released).getFullYear() : 0, dataSource: this.apiName, id: result.id.toString(), image: result.background_image })); } @@ -53,7 +53,7 @@ export class RAWGAPI extends APIModel { const result = response.json as RAWGGame; return new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, - year: result.released ? new Date(result.released).getFullYear().toString() : '', + year: result.released ? new Date(result.released).getFullYear() : 0, dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index b8e72b25..8d8f78a0 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.ts @@ -3,7 +3,7 @@ import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; -import { imageUrlExists } from '../../utils/Utils'; +import { coerceYear, imageUrlExists } from '../../utils/Utils'; import { APIModel } from '../APIModel'; interface SearchResponse { @@ -167,7 +167,7 @@ export class SteamAPI extends APIModel { type: MediaType.Game, title: result.name, englishTitle: result.name, - year: '', + year: 0, dataSource: this.apiName, id: result.appid, }), @@ -219,7 +219,7 @@ export class SteamAPI extends APIModel { type: MediaType.Game, title: result.name, englishTitle: result.name, - year: new Date(result.release_date.date).getFullYear().toString(), + year: coerceYear(new Date(result.release_date.date).getFullYear()), dataSource: this.apiName, url: `https://store.steampowered.com/app/${result.steam_appid}`, id: result.steam_appid.toString(), diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 66676078..db6a137a 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -74,7 +74,7 @@ export class TMDBMovieAPI extends APIModel { type: 'movie', title: result.original_title, englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + year: result.release_date ? new Date(result.release_date).getFullYear() : 0, dataSource: this.apiName, id: result.id.toString(), }), @@ -123,7 +123,7 @@ export class TMDBMovieAPI extends APIModel { type: 'movie', title: result.title, englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear().toString() : 'unknown', + year: result.release_date ? new Date(result.release_date).getFullYear() : 0, premiere: this.plugin.dateFormatter.format(result.release_date, this.apiDateFormat) ?? 'unknown', dataSource: this.apiName, url: `https://www.themoviedb.org/movie/${result.id}`, diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 2fe25232..1c456f5e 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -93,7 +93,7 @@ export class TMDBSeasonAPI extends APIModel { new SeasonModel({ title: `${result.name ?? result.original_name ?? ''}`, englishTitle: result.name ?? result.original_name ?? '', - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + year: result.first_air_date ? new Date(result.first_air_date).getFullYear() : 0, dataSource: this.apiName, id: result.id?.toString() ?? '', seasonTitle: result.name ?? result.original_name ?? '', @@ -144,7 +144,7 @@ export class TMDBSeasonAPI extends APIModel { new SeasonModel({ title: titleText, englishTitle: titleText, - year: season.air_date ? new Date(season.air_date).getFullYear().toString() : 'unknown', + year: season.air_date ? new Date(season.air_date).getFullYear() : 0, dataSource: this.apiName, id: `${tvId}/season/${seasonNumber}`, seasonTitle: season.name ?? titleText, @@ -243,7 +243,7 @@ export class TMDBSeasonAPI extends APIModel { return new SeasonModel({ title: titleText, englishTitle: titleText, - year: airDate ? new Date(airDate).getFullYear().toString() : 'unknown', + year: airDate ? new Date(airDate).getFullYear() : 0, dataSource: this.apiName, url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`, id: `${tvId}/season/${seasonData.season_number}`, diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 96b8d9ee..fab74e30 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -74,7 +74,7 @@ export class TMDBSeriesAPI extends APIModel { type: 'series', title: result.original_name, englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + year: result.first_air_date ? new Date(result.first_air_date).getFullYear() : 0, dataSource: this.apiName, id: result.id.toString(), }), @@ -123,7 +123,7 @@ export class TMDBSeriesAPI extends APIModel { type: 'series', title: result.original_name, englishTitle: result.name, - year: result.first_air_date ? new Date(result.first_air_date).getFullYear().toString() : 'unknown', + year: result.first_air_date ? new Date(result.first_air_date).getFullYear() : 0, dataSource: this.apiName, url: `https://www.themoviedb.org/tv/${result.id}`, id: result.id.toString(), diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index 0139d4ed..2a933dae 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -3,6 +3,7 @@ import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; enum VNDevStatus { @@ -190,7 +191,9 @@ export class VNDBAPI extends APIModel { type: MediaType.Game, title: vn.title, englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear().toString() : 'TBA', + year: coerceYear( + vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear() : 0, + ), dataSource: this.apiName, id: vn.id, }), @@ -228,7 +231,7 @@ export class VNDBAPI extends APIModel { type: MediaType.Game, title: vn.title, englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: releasedIsDate ? new Date(vn.released).getFullYear().toString() : vn.released, + year: coerceYear(releasedIsDate ? new Date(vn.released).getFullYear() : vn.released), dataSource: this.apiName, url: `https://vndb.org/${vn.id}`, id: vn.id, diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts index 9b3b2896..d4a62bcc 100644 --- a/src/api/apis/WikipediaAPI.ts +++ b/src/api/apis/WikipediaAPI.ts @@ -68,7 +68,7 @@ export class WikipediaAPI extends APIModel { type: 'wiki', title: result.title, englishTitle: result.title, - year: '', + year: 0, dataSource: this.apiName, id: result.pageid.toString(), }), diff --git a/src/main.ts b/src/main.ts index a89b3b3e..d5a874d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -343,7 +343,7 @@ export default class MediaDbPlugin extends Plugin { season_number: s.seasonNumber, name: s.seasonTitle || s.title, episode_count: s.episodes || 0, - air_date: s.year, + air_date: s.year > 0 ? String(s.year) : 'unknown', poster_path: s.image, })), true, diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index e4d03a09..c2c69761 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -5,7 +5,7 @@ export abstract class MediaTypeModel { subType: string; title: string; englishTitle: string; - year: string; + year: number; dataSource: string; url: string; id: string; @@ -18,7 +18,7 @@ export abstract class MediaTypeModel { this.subType = ''; this.title = ''; this.englishTitle = ''; - this.year = ''; + this.year = 0; this.dataSource = ''; this.url = ''; this.id = ''; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 69546a2d..c8eda428 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -204,9 +204,37 @@ export interface CreateNoteOptions { folder?: TFolder; } +/** Normalizes release year for metadata: integer, 0 when unknown or non-numeric. */ +export function coerceYear(value: unknown): number { + if (value === undefined || value === null) return 0; + if (typeof value === 'number') { + const n = Math.trunc(value); + return Number.isFinite(n) ? n : 0; + } + if (typeof value === 'string') { + const t = value.trim(); + if (t === '' || t.toLowerCase() === 'unknown' || t === 'TBA' || t.toUpperCase() === 'N/A') { + return 0; + } + const n = parseInt(t, 10); + return Number.isFinite(n) ? n : 0; + } + return 0; +} + export function migrateObject(object: T, oldData: Record, defaultData: T): void { for (const key in object) { - object[key] = Object.hasOwn(oldData, key) && oldData[key] !== undefined && oldData[key] !== null ? (oldData[key] as T[typeof key]) : defaultData[key]; + const has = Object.hasOwn(oldData, key) && oldData[key] !== undefined && oldData[key] !== null; + if (!has) { + object[key] = defaultData[key]; + continue; + } + const raw = oldData[key]; + if (key === 'year') { + (object as Record)[key] = coerceYear(raw); + continue; + } + object[key] = raw as T[typeof key]; } } From 9d3de01634b49a09cf22a3a3710134b2977c920a Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 16:57:56 -0700 Subject: [PATCH 13/60] Fix wikilinks formation for Artists and Album, fix file name sanitizer to keep square brackets, fix bug where songs were not being fetched due to 401 error from Genius --- src/api/GeniusClient.ts | 8 +- src/main.ts | 1 - src/settings/PropertyMapper.ts | 101 ++++++++++++++++++++- src/settings/Settings.ts | 2 +- src/utils/IllegalFilenameCharactersList.ts | 2 - 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/api/GeniusClient.ts b/src/api/GeniusClient.ts index 4f9bf6b9..154e9e5c 100644 --- a/src/api/GeniusClient.ts +++ b/src/api/GeniusClient.ts @@ -72,6 +72,7 @@ export class GeniusClient { 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()}`, @@ -79,7 +80,11 @@ export class GeniusClient { }); if (res.status !== 200) { - console.warn(`MDB | Genius search returned ${res.status}`); + 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; } @@ -95,6 +100,7 @@ export class GeniusClient { async fetchLyricsFromSongPage(songPageUrl: string): Promise { const res = await requestUrl({ url: songPageUrl, + throw: false, headers: { 'User-Agent': this.userAgent, }, diff --git a/src/main.ts b/src/main.ts index d5a874d2..9f43e761 100644 --- a/src/main.ts +++ b/src/main.ts @@ -552,7 +552,6 @@ export default class MediaDbPlugin extends Plugin { const bandSeg = this.safeFileTreeSegment(band.title); const treeRootPath = normalizePath(`${bandBaseFolder.path}/${bandSeg}`); albumNotesFolder = await this.ensureVaultFolder(treeRootPath); - bandNoteFolder = albumNotesFolder; } await this.createStandardMediaDbNoteFromModel(band, { ...options, folder: bandNoteFolder }); diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 27e58712..f0ed8c58 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,5 +1,9 @@ import type MediaDbPlugin from '../main'; +import { BandModel } from '../models/BandModel'; +import { MusicReleaseModel } from '../models/MusicReleaseModel'; +import { MediaType } from '../utils/MediaType'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; +import { coerceYear } from '../utils/Utils'; import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { @@ -41,11 +45,33 @@ export class PropertyMapper { if (propertyMapping.property === key) { let finalValue = value; if (propertyMapping.wikilink) { + const useBandFileNameForArtists = + propertyMapping.property === 'artists' && + (internalMediaType === MediaType.Song || internalMediaType === MediaType.MusicRelease); + const useMusicReleaseFileNameForAlbumTitle = + propertyMapping.property === 'albumTitle' && internalMediaType === MediaType.Song; + if (typeof value === 'string') { - finalValue = `[[${value}]]`; + if (useBandFileNameForArtists) { + finalValue = this.bandArtistWikilink(value); + } else if (useMusicReleaseFileNameForAlbumTitle) { + finalValue = this.songAlbumTitleWikilink(value, obj); + } else { + finalValue = `[[${value}]]`; + } } else if (Array.isArray(value)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - finalValue = value.map(v => (typeof v === 'string' ? `[[${v}]]` : v)); + finalValue = value.map((v: unknown) => { + if (typeof v !== 'string') { + return v; + } + if (useBandFileNameForArtists) { + return this.bandArtistWikilink(v); + } + if (useMusicReleaseFileNameForAlbumTitle) { + return this.songAlbumTitleWikilink(v, obj); + } + return `[[${v}]]`; + }); } } if (propertyMapping.mapping === PropertyMappingOption.Map) { @@ -90,7 +116,10 @@ export class PropertyMapper { console.debug(`MDB | updated metadata type`, typeVal); } const typeStr = String(typeVal).trim(); - if (typeStr === model.type || typeStr === noteTypeValueForMedia(this.plugin.settings, model.type)) { + if ( + typeStr === (model.type as string) || + typeStr === noteTypeValueForMedia(this.plugin.settings, model.type) + ) { matchedModel = model; break; } @@ -123,4 +152,68 @@ export class PropertyMapper { return originalObj; } + + /** + * Wikilink for an artist name using the Band file name template as the link target and the raw artist title as the display alias. + */ + private bandArtistWikilink(artistTitle: string): string { + const title = artistTitle.trim(); + const bandModel = new BandModel({ + type: 'band', + title, + englishTitle: title, + year: 0, + beginYear: '', + releaseDate: '', + dataSource: '', + url: '', + id: '', + country: '', + disambiguation: '', + genres: [], + image: '', + officialWebsite: '', + subType: 'band', + userData: { personalRating: 0 }, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(bandModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; + } + + /** + * Wikilink for a song's album title using the Music Release file name template; fills artists/year from the song metadata when present. + */ + private songAlbumTitleWikilink(albumTitle: string, songMeta: Record): string { + const title = albumTitle.trim(); + const artistsRaw = songMeta.artists; + const artists = Array.isArray(artistsRaw) + ? artistsRaw.filter((a): a is string => typeof a === 'string') + : []; + const year = coerceYear(songMeta.year); + const releaseModel = new MusicReleaseModel({ + type: 'musicRelease', + title, + englishTitle: title, + year, + releaseDate: '', + dataSource: '', + url: '', + id: '', + image: '', + artists, + genres: [], + subType: 'album', + language: '', + rating: 0, + userData: { personalRating: 0 }, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; + } } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d8db7fc4..e007e5e5 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -424,7 +424,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { mangaFileNameTemplate: '{{ title }} ({{ year }})', gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', - musicReleaseFileNameTemplate: '{{ title }} (by {{ ENUM:artists }} - {{ year }})', + musicReleaseFileNameTemplate: '{{ title }} ({{ FIRST:artists }} - {{ year }})', bandFileNameTemplate: '{{ title }}', songFileNameTemplate: '{{ trackNumber }}. {{ title }} ({{ albumTitle }})', boardgameFileNameTemplate: '{{ title }} ({{ year }})', diff --git a/src/utils/IllegalFilenameCharactersList.ts b/src/utils/IllegalFilenameCharactersList.ts index 220ca9ad..23332276 100644 --- a/src/utils/IllegalFilenameCharactersList.ts +++ b/src/utils/IllegalFilenameCharactersList.ts @@ -9,8 +9,6 @@ export const ILLEGAL_FILENAME_CHARACTERS = [ ['|', ' - '], ['?', ''], ['*', ''], - ['[', '('], - [']', ')'], ['^', ''], ['#', ''], ]; From 0d38b5d89051cf4ce536ffa2e1831d5703b625d7 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 17:08:10 -0700 Subject: [PATCH 14/60] make property mapper auto save --- src/modals/PropertyMappingModal.ts | 5 ++- .../PropertyMappingModelComponent.tsx | 35 +++++-------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/modals/PropertyMappingModal.ts b/src/modals/PropertyMappingModal.ts index d8c39e75..8f5adccd 100644 --- a/src/modals/PropertyMappingModal.ts +++ b/src/modals/PropertyMappingModal.ts @@ -1,5 +1,5 @@ import type { App } from 'obsidian'; -import { Modal, Notice } from 'obsidian'; +import { Modal } from 'obsidian'; import { render } from 'solid-js/web'; import type MediaDbPlugin from '../main'; import type { MediaType } from '../utils/MediaType'; @@ -30,7 +30,7 @@ export class PropertyMappingModal extends Modal { contentEl.createEl('p', { cls: 'mod-muted', - text: 'Choose whether each metadata field stays as-is, is renamed in front matter, or is omitted. Use Save to persist your changes.', + text: 'Choose whether each metadata field stays as-is, is renamed in front matter, or is omitted. Changes are saved automatically when valid.', }); const root = contentEl.createDiv(); @@ -44,7 +44,6 @@ export class PropertyMappingModal extends Modal { if (index !== -1) { this.plugin.settings.propertyMappingModels[index] = model; } - new Notice(`MDB: Property mappings for ${mediaTypeDisplayName(model.type)} saved successfully.`); void this.plugin.saveSettings(); }, }), diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index 29f00c42..a533687a 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -1,4 +1,4 @@ -import { createSignal, createMemo, For, Show } from 'solid-js'; +import { createMemo, For, Show } from 'solid-js'; import { createStore } from 'solid-js/store'; import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; import type { MediaType } from '../utils/MediaType'; @@ -13,8 +13,6 @@ interface PropertyMappingModelComponentProps { } export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { - const [unsavedChanges, setUnsavedChanges] = createSignal(false); - // Create a store from the model's plain data const [modelData, setModelData] = createStore(props.model); @@ -24,15 +22,10 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode return model.validate(); }); - const onModelUpdate = () => { - setUnsavedChanges(true); - }; - - const handleSave = () => { + const persistIfValid = () => { const model = PropertyMappingModel.fromJSON(modelData); if (model.validate().res) { props.save(model); - setUnsavedChanges(false); } }; @@ -40,23 +33,11 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode return (
-
- + +
{mediaTypeDisplayName(modelData.type as MediaType)}
- - -
- -
Unsaved changes
-
- -
-
+
{validationResult().err?.message}
@@ -95,7 +76,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode onChange={e => { setModelData('properties', index(), 'mapping', e.currentTarget.value as PropertyMappingOption); setModelData('properties', index(), 'newProperty', ''); - onModelUpdate(); + persistIfValid(); }} > {remappingOption => } @@ -116,7 +97,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode value={property.newProperty} onInput={e => { setModelData('properties', index(), 'newProperty', e.currentTarget.value); - onModelUpdate(); + persistIfValid(); }} />
@@ -130,7 +111,7 @@ export default function PropertyMappingModelComponent(props: PropertyMappingMode checked={property.wikilink} onChange={e => { setModelData('properties', index(), 'wikilink', e.currentTarget.checked); - onModelUpdate(); + persistIfValid(); }} /> From b14bfddf18f8405e8b6da2bdd3feb2414cca6c3b Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Sun, 29 Mar 2026 17:26:54 -0700 Subject: [PATCH 15/60] Add setting for normalized title as Alias --- src/main.ts | 52 ++++++++++++++++++++++------- src/settings/PropertyMapper.ts | 44 ++++++++++++++++++++++++ src/settings/Settings.ts | 18 ++++++++++ src/utils/normalizeTitleForAlias.ts | 22 ++++++++++++ 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 src/utils/normalizeTitleForAlias.ts diff --git a/src/main.ts b/src/main.ts index 9f43e761..c67db66f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/n import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; import type { CreateNoteOptions } from './utils/Utils'; +import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; import 'src/styles.css'; @@ -670,9 +671,44 @@ export default class MediaDbPlugin extends Plugin { return false; } + private metadataRecordForNewNote(mediaTypeModel: MediaTypeModel): Record { + let meta: Record; + if (this.settings.useDefaultFrontMatter) { + meta = mediaTypeModel.toMetaDataObject(); + } else { + meta = { + id: mediaTypeModel.id, + type: mediaTypeModel.type, + dataSource: mediaTypeModel.dataSource, + }; + } + return this.withNormalizedTitleAliasMetadata(meta, mediaTypeModel.title); + } + + private withNormalizedTitleAliasMetadata(meta: Record, title: string): Record { + if (!this.settings.addNormalizeTitlesAsAlias) { + return meta; + } + const alias = normalizeTitleForAsciiAlias(title); + if (alias === null) { + return meta; + } + const prev = meta['aliases']; + let list: string[] = []; + if (Array.isArray(prev)) { + list = prev.filter((x): x is string => typeof x === 'string'); + } else if (typeof prev === 'string' && prev.length > 0) { + list = [prev]; + } + if (!list.includes(alias)) { + list = [...list, alias]; + } + return { ...meta, aliases: list }; + } + generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); - const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + const fileMetadata = this.modelPropertyMapper.convertObject(this.metadataRecordForNewNote(mediaTypeModel)); return stringifyYaml(fileMetadata); } @@ -686,17 +722,9 @@ export default class MediaDbPlugin extends Plugin { mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - let fileMetadata: Record; - - if (this.settings.useDefaultFrontMatter) { - fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - } else { - fileMetadata = { - id: mediaTypeModel.id, - type: mediaTypeModel.type, - dataSource: mediaTypeModel.dataSource, - }; - } + let fileMetadata: Record = this.modelPropertyMapper.convertObject( + this.metadataRecordForNewNote(mediaTypeModel), + ); let fileContent = ''; template = options.attachTemplate ? template : ''; diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index f0ed8c58..a3c594bc 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -41,6 +41,9 @@ export class PropertyMapper { const newObj: Record = {}; for (const [key, value] of Object.entries(obj)) { + if (key === 'aliases') { + continue; + } for (const propertyMapping of propertyMappings) { if (propertyMapping.property === key) { let finalValue = value; @@ -88,9 +91,50 @@ export class PropertyMapper { } } + if (Object.hasOwn(obj, 'aliases')) { + const aliasesPm = propertyMappings.find(p => p.property === 'aliases'); + if (aliasesPm?.mapping !== PropertyMappingOption.Remove) { + const incoming = obj['aliases']; + const targetKey = + aliasesPm?.mapping === PropertyMappingOption.Map && aliasesPm.newProperty + ? aliasesPm.newProperty + : 'aliases'; + const merged = PropertyMapper.mergeAliasValues(newObj[targetKey], incoming); + if (merged.length > 0) { + newObj[targetKey] = merged; + } + } + } + return newObj; } + private static mergeAliasValues(existing: unknown, added: unknown): string[] { + const toStrings = (v: unknown): string[] => { + if (v == null) { + return []; + } + if (Array.isArray(v)) { + return v.flatMap(x => (typeof x === 'string' ? x : String(x))).filter(s => s.length > 0); + } + if (typeof v === 'string') { + return v.length > 0 ? [v] : []; + } + return []; + }; + + const combined = [...toStrings(existing), ...toStrings(added)]; + const seen = new Set(); + const out: string[] = []; + for (const s of combined) { + if (!seen.has(s)) { + seen.add(s); + out.push(s); + } + } + return out; + } + /** * Converts an object back using the conversion rules for its type. * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index e007e5e5..ad42490f 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -57,6 +57,8 @@ export interface MediaDbPluginSettings { customDateFormat: string; openNoteInNewTab: boolean; useDefaultFrontMatter: boolean; + /** When true, add an Obsidian `aliases` entry with an ASCII form of the title when it uses diacritics or letters like ø (e.g. Likbør → Likbor). */ + addNormalizeTitlesAsAlias: boolean; enableTemplaterIntegration: boolean; imageDownload: boolean; imageFolder: string; @@ -383,6 +385,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { customDateFormat: 'L', openNoteInNewTab: true, useDefaultFrontMatter: true, + addNormalizeTitlesAsAlias: true, enableTemplaterIntegration: false, imageDownload: false, imageFolder: 'Media DB/images', @@ -843,6 +846,21 @@ export class MediaDbSettingTab extends PluginSettingTab { }), ); + generalGroup.addSetting( + setting => + void setting + .setName('Add Normalize Titles as Alias') + .setDesc( + 'When the title uses accented or special Latin letters (e.g. ó, ø), add an ASCII form to YAML aliases so links without those characters still resolve (e.g. Likbør → Likbor).', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.addNormalizeTitlesAsAlias).onChange(data => { + this.plugin.settings.addNormalizeTitlesAsAlias = data; + void this.plugin.saveSettings(); + }); + }), + ); + generalGroup.addSetting( setting => void setting diff --git a/src/utils/normalizeTitleForAlias.ts b/src/utils/normalizeTitleForAlias.ts new file mode 100644 index 00000000..cdb4b477 --- /dev/null +++ b/src/utils/normalizeTitleForAlias.ts @@ -0,0 +1,22 @@ +/** + * ASCII-style form of a title for use as an Obsidian `aliases` entry (e.g. Likbør → Likbor). + * Returns null when the title should not get an extra alias (unchanged after normalization). + */ +export function normalizeTitleForAsciiAlias(title: string): string | null { + const trimmed = title.trim(); + if (!trimmed) { + return null; + } + + let s = trimmed.normalize('NFKD').replace(/\p{M}/gu, ''); + s = s + .replaceAll('ø', 'o') + .replaceAll('Ø', 'O') + .replaceAll('ß', 'ss') + .replaceAll('ẞ', 'SS'); + + if (s === trimmed) { + return null; + } + return s; +} From 548f25afc51739d0badd46e0bbd4b9f23a9467c7 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 15:37:27 -0700 Subject: [PATCH 16/60] Update obsidian module to latest version --- package-lock.json | 50 ++++++++++++++--------------------------------- package.json | 2 +- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79e04a64..20cdd5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,11 @@ "@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", @@ -294,22 +293,20 @@ } }, "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "node_modules/@codemirror/view": { - "version": "6.37.2", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", - "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", @@ -1008,7 +1005,6 @@ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/@popperjs/core": { @@ -2162,18 +2158,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/builtin-modules": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", - "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", - "dev": true, - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bun-types": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", @@ -2368,7 +2352,6 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/cross-spawn": { @@ -4281,18 +4264,17 @@ } }, "node_modules/obsidian": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", - "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", "dev": true, - "license": "MIT", "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" } }, "node_modules/openapi-fetch": { @@ -5109,11 +5091,10 @@ } }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/supports-color": { @@ -5615,7 +5596,6 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/whatwg-mimetype": { diff --git a/package.json b/package.json index 1a301c09..9ba44a85 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "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", From f160f933c39e225b910dd11035d1239722700770 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 16:14:39 -0700 Subject: [PATCH 17/60] Clean up code --- package-lock.json | 55 ------------------------- package.json | 2 - src/api/apis/BoardGameGeekAPI.ts | 6 +-- src/api/apis/ComicVineAPI.ts | 6 +-- src/api/apis/GiantBombAPI.ts | 12 +++--- src/api/apis/IGDBAPI.ts | 10 ++--- src/api/apis/MobyGamesAPI.ts | 12 +++--- src/api/apis/OMDbAPI.ts | 6 +-- src/api/apis/RAWGAPI.ts | 12 +++--- src/api/apis/TMDBMovieAPI.ts | 12 +++--- src/api/apis/TMDBSeasonAPI.ts | 21 +++++----- src/api/apis/TMDBSeriesAPI.ts | 12 +++--- src/main.ts | 7 ++-- src/obsidian-secrets-augment.d.ts | 25 ------------ src/obsidian-setting-group.d.ts | 12 ------ src/settings/Settings.ts | 59 --------------------------- src/settings/apiSecretHelpers.ts | 67 ------------------------------- tsconfig.json | 4 +- 18 files changed, 66 insertions(+), 274 deletions(-) delete mode 100644 src/obsidian-secrets-augment.d.ts delete mode 100644 src/obsidian-setting-group.d.ts delete mode 100644 src/settings/apiSecretHelpers.ts diff --git a/package-lock.json b/package-lock.json index 20cdd5d0..4f1d5a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,7 @@ "version": "0.8.0", "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", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", @@ -867,19 +865,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@happy-dom/global-registrator": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/@happy-dom/global-registrator/-/global-registrator-18.0.1.tgz", - "integrity": "sha512-xCy/cpEP8xyJ6u0eokYgaQxeUmcKqHx/+aC3R0DLa7/S38efhZAVDQqLJ5zzTguLFS0gvAzZHP40NGaLwRyapQ==", - "dev": true, - "dependencies": { - "@types/node": "^20.0.0", - "happy-dom": "^18.0.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1007,17 +992,6 @@ "dev": true, "peer": true }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@redocly/ajv": { "version": "8.17.2", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.2.tgz", @@ -1519,12 +1493,6 @@ "@types/estree": "*" } }, - "node_modules/@types/whatwg-mimetype": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", - "dev": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -3299,20 +3267,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/happy-dom": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz", - "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", - "dev": true, - "dependencies": { - "@types/node": "^20.0.0", - "@types/whatwg-mimetype": "^3.0.2", - "whatwg-mimetype": "^3.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5598,15 +5552,6 @@ "dev": true, "peer": true }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9ba44a85..a6e03523 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,7 @@ "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", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index 8c4929a3..c77f5ad7 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -1,7 +1,7 @@ import { requestUrl } from 'obsidian'; import { BoardGameModel } from 'src/models/BoardGameModel'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { coerceYear } from '../../utils/Utils'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; @@ -25,7 +25,7 @@ export class BoardGameGeekAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const bggKey = apiSecrets.boardgameGeek(this.plugin); + const bggKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.boardgameGeek) ?? ''; if (!bggKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -75,7 +75,7 @@ export class BoardGameGeekAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const bggKey = apiSecrets.boardgameGeek(this.plugin); + const bggKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.boardgameGeek) ?? ''; if (!bggKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index bcc48cf9..24484fa5 100644 --- a/src/api/apis/ComicVineAPI.ts +++ b/src/api/apis/ComicVineAPI.ts @@ -3,8 +3,8 @@ import { requestUrl } from 'obsidian'; import { ComicMangaModel } from 'src/models/ComicMangaModel'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -27,7 +27,7 @@ export class ComicVineAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const apiKey = apiSecrets.comicVine(this.plugin); + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.comicVine) ?? ''; if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -63,7 +63,7 @@ export class ComicVineAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const apiKey = apiSecrets.comicVine(this.plugin); + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.comicVine) ?? ''; if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 36185f1d..d114c916 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -1,8 +1,8 @@ import createClient from 'openapi-fetch'; import { coerceYear, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import { GameModel } from '../../models/GameModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -25,7 +25,8 @@ export class GiantBombAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!apiSecrets.giantBomb(this.plugin)) { + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.giantBomb) ?? ''; + if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -33,7 +34,7 @@ export class GiantBombAPI extends APIModel { const response = await client.GET('/games', { params: { query: { - api_key: apiSecrets.giantBomb(this.plugin), + api_key: apiKey, filter: `name:${title}`, format: 'json', limit: 20, @@ -75,7 +76,8 @@ export class GiantBombAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!apiSecrets.giantBomb(this.plugin)) { + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.giantBomb) ?? ''; + if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -86,7 +88,7 @@ export class GiantBombAPI extends APIModel { guid: id, }, query: { - api_key: apiSecrets.giantBomb(this.plugin), + api_key: apiKey, format: 'json', }, }, diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index ea05a9b0..b03d262c 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -2,7 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -37,8 +37,8 @@ export class IGDBAPI extends APIModel { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - const clientId = apiSecrets.igdbClientId(this.plugin); - const clientSecret = apiSecrets.igdbClientSecret(this.plugin); + const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; + const clientSecret = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientSecret) ?? ''; if (!clientId || !clientSecret) { throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); } @@ -57,7 +57,7 @@ export class IGDBAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); const token = await this.getAuthToken(); - const clientId = apiSecrets.igdbClientId(this.plugin); + const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', @@ -80,7 +80,7 @@ export class IGDBAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); const token = await this.getAuthToken(); - const clientId = apiSecrets.igdbClientId(this.plugin); + const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index 2e9ab1d6..baa5f6db 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -2,8 +2,8 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import { GameModel } from '../../models/GameModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,11 +29,12 @@ export class MobyGamesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!apiSecrets.mobyGames(this.plugin)) { + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.mobyGames) ?? ''; + if (!apiKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${apiSecrets.mobyGames(this.plugin)}`; + const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${apiKey}`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -72,11 +73,12 @@ export class MobyGamesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!apiSecrets.mobyGames(this.plugin)) { + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.mobyGames) ?? ''; + if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${apiSecrets.mobyGames(this.plugin)}`; + const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${apiKey}`; const fetchData = await requestUrl({ url: searchUrl, }); diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 39a35acd..3161a00c 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -4,7 +4,7 @@ import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import { SeriesModel } from '../../models/SeriesModel'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -79,7 +79,7 @@ export class OMDbAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const omdbKey = apiSecrets.omdb(this.plugin); + const omdbKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.omdb) ?? ''; if (!omdbKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -164,7 +164,7 @@ export class OMDbAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const omdbKey = apiSecrets.omdb(this.plugin); + const omdbKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.omdb) ?? ''; if (!omdbKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index cdff1690..3937cb1d 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -2,7 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -27,9 +27,10 @@ export class RAWGAPI extends APIModel { } async searchByTitle(title: string): Promise { - if (!apiSecrets.rawg(this.plugin)) throw Error(`MDB | API key for ${this.apiName} missing.`); + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.rawg) ?? ''; + if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${apiSecrets.rawg(this.plugin)}&search=${encodeURIComponent(title)}&page_size=20`, + url: `${this.apiUrl}/games?key=${apiKey}&search=${encodeURIComponent(title)}&page_size=20`, method: 'GET', }); if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); @@ -43,9 +44,10 @@ export class RAWGAPI extends APIModel { } async getById(id: string): Promise { - if (!apiSecrets.rawg(this.plugin)) throw Error(`MDB | API key for ${this.apiName} missing.`); + const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.rawg) ?? ''; + if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${apiSecrets.rawg(this.plugin)}`, + url: `${this.apiUrl}/games/${id}?key=${apiKey}`, method: 'GET', }); if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index db6a137a..4790550b 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -2,8 +2,8 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { MovieModel } from '../../models/MovieModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,14 +29,15 @@ export class TMDBMovieAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/search/movie', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { query: { @@ -87,14 +88,15 @@ export class TMDBMovieAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/movie/{movie_id}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { movie_id: parseInt(id) }, diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 1c456f5e..1e263e66 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -2,8 +2,8 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { SeasonModel } from '../../models/SeasonModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,14 +29,15 @@ export class TMDBSeasonAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const searchResponse = await client.GET('/3/search/tv', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { query: { @@ -71,7 +72,7 @@ export class TMDBSeasonAPI extends APIModel { try { const detailsResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { series_id: result.id ?? 0 }, @@ -107,14 +108,15 @@ export class TMDBSeasonAPI extends APIModel { // Fetch all seasons for a given series async getSeasonsForSeries(tvId: string): Promise { - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const seriesResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { series_id: parseInt(tvId) }, @@ -160,7 +162,8 @@ export class TMDBSeasonAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -178,7 +181,7 @@ export class TMDBSeasonAPI extends APIModel { // Fetch season details const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { @@ -204,7 +207,7 @@ export class TMDBSeasonAPI extends APIModel { // Fetch parent series to build consistent titles and inherit fields const seriesResponse = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { series_id: parseInt(tvId) }, diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index fab74e30..7e9f8ac3 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -2,8 +2,8 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; -import { apiSecrets } from '../../settings/apiSecretHelpers'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { API_SECRET_IDS } from '../../settings/apiSecretIds'; import { SeriesModel } from '../../models/SeriesModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,14 +29,15 @@ export class TMDBSeriesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/search/tv', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { query: { @@ -87,14 +88,15 @@ export class TMDBSeriesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - if (!apiSecrets.tmdb(this.plugin)) { + const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); const response = await client.GET('/3/tv/{series_id}', { headers: { - Authorization: `Bearer ${apiSecrets.tmdb(this.plugin)}`, + Authorization: `Bearer ${bearer}`, }, params: { path: { series_id: parseInt(id) }, diff --git a/src/main.ts b/src/main.ts index c67db66f..86026f40 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,6 @@ import type { MediaTypeModel } from './models/MediaTypeModel'; import type { MusicReleaseModel } from './models/MusicReleaseModel'; import type { SeasonModel } from './models/SeasonModel'; import { SongModel } from './models/SongModel'; -import { migrateLegacyApiKeysToSecretStorage, stripPlaintextApiKeysFromSettings } from './settings/apiSecretHelpers'; import { API_SECRET_IDS } from './settings/apiSecretIds'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; @@ -735,7 +734,9 @@ export default class MediaDbPlugin extends Plugin { if (mediaTypeModel.getMediaType() === MediaType.Song) { const song = mediaTypeModel as SongModel; const body = (song.lyrics ?? '').trim(); - fileContent += `\n\n# Lyrics\n\n${body.length > 0 ? body : '_Lyrics not found._'}\n`; + if(body.length > 0) { + fileContent += `# Lyrics\n\`\`\`\n${body}\n\`\`\`\n`; + } } if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { @@ -933,11 +934,9 @@ export default class MediaDbPlugin extends Plugin { loadedSettings.propertyMappingModels = propertyMappingModelsInDisplayOrder(migratedModels.map(m => m.toJSON())); this.settings = loadedSettings; - migrateLegacyApiKeysToSecretStorage(this); } async saveSettings(): Promise { - stripPlaintextApiKeysFromSettings(this.settings); this.mediaTypeManager.updateTemplates(this.settings); this.mediaTypeManager.updateFolders(this.settings); this.dateFormatter.setFormat(this.settings.customDateFormat); diff --git a/src/obsidian-secrets-augment.d.ts b/src/obsidian-secrets-augment.d.ts deleted file mode 100644 index 2d4cdc65..00000000 --- a/src/obsidian-secrets-augment.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { App, BaseComponent } from 'obsidian'; - -declare module 'obsidian' { - /** @public @since 1.11.4 */ - export class SecretStorage { - setSecret(id: string, secret: string): void; - getSecret(id: string): string | null; - listSecrets(): string[]; - } - - /** @public @since 1.11.1 */ - export class SecretComponent extends BaseComponent { - constructor(app: App, containerEl: HTMLElement); - setValue(value: string): this; - onChange(cb: (value: string) => unknown): this; - } - - interface App { - secretStorage: SecretStorage; - } - - interface Setting { - addComponent(cb: (el: HTMLElement) => T): this; - } -} diff --git a/src/obsidian-setting-group.d.ts b/src/obsidian-setting-group.d.ts deleted file mode 100644 index 2018e48b..00000000 --- a/src/obsidian-setting-group.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Setting } from 'obsidian'; - -declare module 'obsidian' { - /** - * Settings section container (Obsidian 1.6+). Types lag the runtime API. - */ - export class SettingGroup { - constructor(containerEl: HTMLElement); - setHeading(name: string): this; - addSetting(cb: (setting: Setting) => unknown): void; - } -} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index ad42490f..159b124d 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -43,15 +43,6 @@ function mediaTypeTabIcon(mediaType: MediaType): IconName { // MARK: Settings export interface MediaDbPluginSettings { - OMDbKey: string; - TMDBKey: string; - MobyGamesKey: string; - GiantBombKey: string; - IGDBClientId: string; - IGDBClientSecret: string; - RAWGAPIKey: string; - ComicVineKey: string; - BoardgameGeekKey: string; sfwFilter: boolean; templates: boolean; customDateFormat: string; @@ -134,19 +125,6 @@ export interface MediaDbPluginSettings { bookFolder: string; propertyMappingModels: PropertyMappingModelData[]; - - // DEPRECATED: Use propertyMappingModels instead - moviePropertyConversionRules: string; - seriesPropertyConversionRules: string; - seasonPropertyConversionRules: string; - mangaPropertyConversionRules: string; - gamePropertyConversionRules: string; - wikiPropertyConversionRules: string; - musicReleasePropertyConversionRules: string; - bandPropertyConversionRules: string; - songPropertyConversionRules: string; - boardgamePropertyConversionRules: string; - bookPropertyConversionRules: string; } /** @@ -371,15 +349,6 @@ class MediaTypeMappedSettings { // MARK: Defaults const DEFAULT_SETTINGS: MediaDbPluginSettings = { - OMDbKey: '', - TMDBKey: '', - MobyGamesKey: '', - GiantBombKey: '', - IGDBClientId: '', - IGDBClientSecret: '', - RAWGAPIKey: '', - ComicVineKey: '', - BoardgameGeekKey: '', sfwFilter: true, templates: true, customDateFormat: 'L', @@ -459,19 +428,6 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { bookNoteType: '', propertyMappingModels: [], - - // DEPRECATED - moviePropertyConversionRules: '', - seriesPropertyConversionRules: '', - seasonPropertyConversionRules: '', - mangaPropertyConversionRules: '', - gamePropertyConversionRules: '', - wikiPropertyConversionRules: '', - musicReleasePropertyConversionRules: '', - bandPropertyConversionRules: '', - songPropertyConversionRules: '', - boardgamePropertyConversionRules: '', - bookPropertyConversionRules: '', }; export const lockedPropertyMappings: string[] = []; @@ -548,16 +504,10 @@ export class MediaDbSettingTab extends PluginSettingTab { private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Band, MediaType.MusicRelease, MediaType.Song]; - private static readonly LEGACY_MUSIC_TAB_IDS: ReadonlySet = new Set(['media-band', 'media-musicRelease', 'media-song']); - private static readonly BOOK_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Book, MediaType.ComicManga]; - private static readonly LEGACY_BOOK_TAB_IDS: ReadonlySet = new Set(['media-comicManga']); - private static readonly VIDEO_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Season]; - private static readonly LEGACY_VIDEO_TAB_IDS: ReadonlySet = new Set(['media-series', 'media-season']); - private renderMediaTypeSection( panel: HTMLElement, mediaTypeSetting: MediaTypeMappedSettings, @@ -1032,15 +982,6 @@ export class MediaDbSettingTab extends PluginSettingTab { const validIds = new Set(tabEntries.map(t => t.id)); let initialId = this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; - if (MediaDbSettingTab.LEGACY_MUSIC_TAB_IDS.has(initialId) && validIds.has('media-music')) { - initialId = 'media-music'; - } - if (MediaDbSettingTab.LEGACY_BOOK_TAB_IDS.has(initialId) && validIds.has('media-book')) { - initialId = 'media-book'; - } - if (MediaDbSettingTab.LEGACY_VIDEO_TAB_IDS.has(initialId) && validIds.has('media-movie')) { - initialId = 'media-movie'; - } if (!validIds.has(initialId)) { initialId = 'general'; } diff --git a/src/settings/apiSecretHelpers.ts b/src/settings/apiSecretHelpers.ts deleted file mode 100644 index dab3f0d9..00000000 --- a/src/settings/apiSecretHelpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type MediaDbPlugin from 'src/main'; -import type { ApiSecretId } from './apiSecretIds'; -import { API_SECRET_IDS } from './apiSecretIds'; -import type { MediaDbPluginSettings } from './Settings'; - -const LEGACY_KEY_PAIRS: { id: ApiSecretId; legacy: keyof MediaDbPluginSettings }[] = [ - { id: API_SECRET_IDS.omdb, legacy: 'OMDbKey' }, - { id: API_SECRET_IDS.tmdb, legacy: 'TMDBKey' }, - { id: API_SECRET_IDS.mobyGames, legacy: 'MobyGamesKey' }, - { id: API_SECRET_IDS.giantBomb, legacy: 'GiantBombKey' }, - { id: API_SECRET_IDS.igdbClientId, legacy: 'IGDBClientId' }, - { id: API_SECRET_IDS.igdbClientSecret, legacy: 'IGDBClientSecret' }, - { id: API_SECRET_IDS.rawg, legacy: 'RAWGAPIKey' }, - { id: API_SECRET_IDS.comicVine, legacy: 'ComicVineKey' }, - { id: API_SECRET_IDS.boardgameGeek, legacy: 'BoardgameGeekKey' }, -]; - -/** Move plaintext API keys from data.json into Obsidian SecretStorage and clear legacy fields. */ -export function migrateLegacyApiKeysToSecretStorage(plugin: MediaDbPlugin): void { - const storage = plugin.app.secretStorage; - const { settings } = plugin; - let dirty = false; - - for (const { id, legacy } of LEGACY_KEY_PAIRS) { - const plaintext = settings[legacy]; - if (typeof plaintext !== 'string' || plaintext === '') { - continue; - } - if (!storage.getSecret(id)) { - storage.setSecret(id, plaintext); - } - dirty = true; - } - if (dirty) { - stripPlaintextApiKeysFromSettings(settings); - void plugin.saveSettings(); - } -} - -/** Prevent API keys from being written to data.json (defense in depth). */ -export function stripPlaintextApiKeysFromSettings(settings: MediaDbPluginSettings): void { - settings.OMDbKey = ''; - settings.TMDBKey = ''; - settings.MobyGamesKey = ''; - settings.GiantBombKey = ''; - settings.IGDBClientId = ''; - settings.IGDBClientSecret = ''; - settings.RAWGAPIKey = ''; - settings.ComicVineKey = ''; - settings.BoardgameGeekKey = ''; -} - -function getSecret(plugin: MediaDbPlugin, id: ApiSecretId): string { - return plugin.app.secretStorage.getSecret(id) ?? ''; -} - -export const apiSecrets = { - omdb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.omdb), - tmdb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.tmdb), - mobyGames: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.mobyGames), - giantBomb: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.giantBomb), - igdbClientId: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.igdbClientId), - igdbClientSecret: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.igdbClientSecret), - rawg: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.rawg), - comicVine: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.comicVine), - boardgameGeek: (p: MediaDbPlugin): string => getSecret(p, API_SECRET_IDS.boardgameGeek), -}; diff --git a/tsconfig.json b/tsconfig.json index c6eb6724..9dc9afed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "allowSyntheticDefaultImports": true, "jsx": "preserve", "jsxImportSource": "solid-js", - "types": ["vite/client"] + "types": ["vite/client", "bun"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"] } From 37d1127756f29c37ea19f0f301f6d77313fc53b2 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 17:36:03 -0700 Subject: [PATCH 18/60] Fix bug with secret storage --- src/api/apis/BoardGameGeekAPI.ts | 6 ++-- src/api/apis/ComicVineAPI.ts | 6 ++-- src/api/apis/GiantBombAPI.ts | 6 ++-- src/api/apis/IGDBAPI.ts | 10 +++---- src/api/apis/MobyGamesAPI.ts | 6 ++-- src/api/apis/OMDbAPI.ts | 6 ++-- src/api/apis/RAWGAPI.ts | 6 ++-- src/api/apis/TMDBMovieAPI.ts | 6 ++-- src/api/apis/TMDBSeasonAPI.ts | 8 ++--- src/api/apis/TMDBSeriesAPI.ts | 6 ++-- src/main.ts | 6 ++-- src/settings/Settings.ts | 50 +++++++++++++++++++++----------- src/settings/apiSecretIds.ts | 18 ------------ src/settings/apiSecretsHelper.ts | 25 ++++++++++++++++ 14 files changed, 94 insertions(+), 71 deletions(-) delete mode 100644 src/settings/apiSecretIds.ts create mode 100644 src/settings/apiSecretsHelper.ts diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index c77f5ad7..bbe251d5 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -1,7 +1,7 @@ import { requestUrl } from 'obsidian'; import { BoardGameModel } from 'src/models/BoardGameModel'; import type MediaDbPlugin from '../../main'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { coerceYear } from '../../utils/Utils'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; @@ -25,7 +25,7 @@ export class BoardGameGeekAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const bggKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.boardgameGeek) ?? ''; + const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); if (!bggKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -75,7 +75,7 @@ export class BoardGameGeekAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const bggKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.boardgameGeek) ?? ''; + const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); if (!bggKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/ComicVineAPI.ts b/src/api/apis/ComicVineAPI.ts index 24484fa5..1bfa3daa 100644 --- a/src/api/apis/ComicVineAPI.ts +++ b/src/api/apis/ComicVineAPI.ts @@ -4,7 +4,7 @@ import { requestUrl } from 'obsidian'; import { ComicMangaModel } from 'src/models/ComicMangaModel'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -27,7 +27,7 @@ export class ComicVineAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.comicVine) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -63,7 +63,7 @@ export class ComicVineAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.comicVine) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index d114c916..3aba1336 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -2,7 +2,7 @@ import createClient from 'openapi-fetch'; import { coerceYear, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -25,7 +25,7 @@ export class GiantBombAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.giantBomb) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } @@ -76,7 +76,7 @@ export class GiantBombAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.giantBomb) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index b03d262c..2c55315a 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -2,7 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -37,8 +37,8 @@ export class IGDBAPI extends APIModel { const currentTime = Date.now(); if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; - const clientSecret = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientSecret) ?? ''; + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); + const clientSecret = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientSecret); if (!clientId || !clientSecret) { throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); } @@ -57,7 +57,7 @@ export class IGDBAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); const token = await this.getAuthToken(); - const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', @@ -80,7 +80,7 @@ export class IGDBAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); const token = await this.getAuthToken(); - const clientId = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.igdbClientId) ?? ''; + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index baa5f6db..b201ad03 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -3,7 +3,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,7 +29,7 @@ export class MobyGamesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.mobyGames) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); if (!apiKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -73,7 +73,7 @@ export class MobyGamesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.mobyGames) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); if (!apiKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 3161a00c..a96ec4b1 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -4,7 +4,7 @@ import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import { SeriesModel } from '../../models/SeriesModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; @@ -79,7 +79,7 @@ export class OMDbAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const omdbKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.omdb) ?? ''; + const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); if (!omdbKey) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -164,7 +164,7 @@ export class OMDbAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const omdbKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.omdb) ?? ''; + const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); if (!omdbKey) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index 3937cb1d..c22c9a9a 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -2,7 +2,7 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -27,7 +27,7 @@ export class RAWGAPI extends APIModel { } async searchByTitle(title: string): Promise { - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.rawg) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ url: `${this.apiUrl}/games?key=${apiKey}&search=${encodeURIComponent(title)}&page_size=20`, @@ -44,7 +44,7 @@ export class RAWGAPI extends APIModel { } async getById(id: string): Promise { - const apiKey = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.rawg) ?? ''; + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); const response = await requestUrl({ url: `${this.apiUrl}/games/${id}?key=${apiKey}`, diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 4790550b..0444ea0c 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -3,7 +3,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MovieModel } from '../../models/MovieModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,7 +29,7 @@ export class TMDBMovieAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -88,7 +88,7 @@ export class TMDBMovieAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index 1e263e66..b91b50a0 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -3,7 +3,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { SeasonModel } from '../../models/SeasonModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,7 +29,7 @@ export class TMDBSeasonAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -108,7 +108,7 @@ export class TMDBSeasonAPI extends APIModel { // Fetch all seasons for a given series async getSeasonsForSeries(tvId: string): Promise { - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -162,7 +162,7 @@ export class TMDBSeasonAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 7e9f8ac3..8ffd5919 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -3,7 +3,7 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { API_SECRET_IDS } from '../../settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { SeriesModel } from '../../models/SeriesModel'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; @@ -29,7 +29,7 @@ export class TMDBSeriesAPI extends APIModel { async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw new Error(`MDB | API key for ${this.apiName} missing.`); } @@ -88,7 +88,7 @@ export class TMDBSeriesAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const bearer = this.plugin.app.secretStorage.getSecret(API_SECRET_IDS.tmdb) ?? ''; + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); if (!bearer) { throw Error(`MDB | API key for ${this.apiName} missing.`); } diff --git a/src/main.ts b/src/main.ts index 86026f40..6b9fc84e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,7 @@ import type { MediaTypeModel } from './models/MediaTypeModel'; import type { MusicReleaseModel } from './models/MusicReleaseModel'; import type { SeasonModel } from './models/SeasonModel'; import { SongModel } from './models/SongModel'; -import { API_SECRET_IDS } from './settings/apiSecretIds'; +import { ApiSecretID, getApiSecretValue } from './settings/apiSecretsHelper'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; @@ -523,8 +523,8 @@ export default class MediaDbPlugin extends Plugin { private async importBandDiscography(band: BandModel, options: CreateNoteOptions): Promise { try { - const geniusToken = this.app.secretStorage.getSecret(API_SECRET_IDS.genius); - const genius = new GeniusClient(geniusToken ?? undefined); + const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; + const genius = new GeniusClient(geniusToken); if (!genius.isConfigured()) { new Notice('Band import: add a Genius API access token in settings to fetch lyrics.'); } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 159b124d..dd358442 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -2,18 +2,18 @@ import type { App, IconName } from 'obsidian'; import { Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; +import { ApiSecretID } from './apiSecretsHelper'; import { PropertyMappingModal } from '../modals/PropertyMappingModal'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { noteTypeValueForMedia, setNoteTypeForMedia } from '../utils/noteTypeSettings'; import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; -import type { ApiSecretId } from './apiSecretIds'; -import { API_SECRET_IDS } from './apiSecretIds'; import type { PropertyMappingModelData } from './PropertyMapping'; import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; import { FileSuggest } from './suggesters/FileSuggest'; import { FolderSuggest } from './suggesters/FolderSuggest'; + function mediaTypeTabIcon(mediaType: MediaType): IconName { switch (mediaType) { case MediaType.Band: @@ -125,6 +125,7 @@ export interface MediaDbPluginSettings { bookFolder: string; propertyMappingModels: PropertyMappingModelData[]; + linkedApiSecretIds: Record; } /** @@ -428,6 +429,19 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { bookNoteType: '', propertyMappingModels: [], + + linkedApiSecretIds: { + [ApiSecretID.omdb]: '', + [ApiSecretID.tmdb]: '', + [ApiSecretID.mobyGames]: '', + [ApiSecretID.giantBomb]: '', + [ApiSecretID.igdbClientId]: '', + [ApiSecretID.igdbClientSecret]: '', + [ApiSecretID.rawg]: '', + [ApiSecretID.comicVine]: '', + [ApiSecretID.boardgameGeek]: '', + [ApiSecretID.genius]: '', + }, }; export const lockedPropertyMappings: string[] = []; @@ -485,17 +499,19 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin = plugin; } - private addApiSecretSetting(group: SettingGroup, name: string, description: string, secretId: ApiSecretId): void { + private addApiSecretSetting(group: SettingGroup, name: string, description: string, slot: ApiSecretID): void { group.addSetting( setting => - void setting + setting .setName(name) .setDesc(description) .addComponent(el => { const component = new SecretComponent(this.app, el); - component.setValue(this.app.secretStorage.getSecret(secretId) ?? '').onChange(value => { - this.app.secretStorage.setSecret(secretId, value); - void this.plugin.saveSettings(); + const { linkedApiSecretIds } = this.plugin.settings; + const linkedId = linkedApiSecretIds[slot] ?? ''; + component.setValue(linkedId).onChange((secretId: string) => { + linkedApiSecretIds[slot] = secretId; + this.plugin.saveSettings(); }); return component; }), @@ -921,20 +937,20 @@ export class MediaDbSettingTab extends PluginSettingTab { addTab('api-keys', 'API keys', 'key', panel => { const apiKeyGroup = new SettingGroup(panel); - this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', API_SECRET_IDS.omdb); - this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', API_SECRET_IDS.tmdb); - this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', API_SECRET_IDS.mobyGames); - this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', API_SECRET_IDS.giantBomb); - this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', API_SECRET_IDS.igdbClientId); - this.addApiSecretSetting(apiKeyGroup, 'IGDB Client Secret', 'Client Secret for IGDB API.', API_SECRET_IDS.igdbClientSecret); - this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', API_SECRET_IDS.rawg); - this.addApiSecretSetting(apiKeyGroup, 'Comic Vine Key', 'API key for "www.comicvine.gamespot.com".', API_SECRET_IDS.comicVine); - this.addApiSecretSetting(apiKeyGroup, 'Boardgame Geek Key', 'API key for "www.boardgamegeek.com".', API_SECRET_IDS.boardgameGeek); + this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', ApiSecretID.omdb); + this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', ApiSecretID.tmdb); + this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', ApiSecretID.mobyGames); + this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', ApiSecretID.giantBomb); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', ApiSecretID.igdbClientId); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client Secret', 'Client Secret for IGDB API.', ApiSecretID.igdbClientSecret); + this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', ApiSecretID.rawg); + this.addApiSecretSetting(apiKeyGroup, 'Comic Vine Key', 'API key for "www.comicvine.gamespot.com".', ApiSecretID.comicVine); + this.addApiSecretSetting(apiKeyGroup, 'Boardgame Geek Key', 'API key for "www.boardgamegeek.com".', ApiSecretID.boardgameGeek); this.addApiSecretSetting( apiKeyGroup, 'Genius API access token', 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing a band.', - API_SECRET_IDS.genius, + ApiSecretID.genius, ); }); diff --git a/src/settings/apiSecretIds.ts b/src/settings/apiSecretIds.ts deleted file mode 100644 index b2999e85..00000000 --- a/src/settings/apiSecretIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Obsidian SecretStorage IDs (lowercase alphanumeric + dashes only). - * @see https://docs.obsidian.md/plugins/guides/secret-storage - */ -export const API_SECRET_IDS = { - omdb: 'media-db-omdb', - tmdb: 'media-db-tmdb', - mobyGames: 'media-db-moby-games', - giantBomb: 'media-db-giant-bomb', - igdbClientId: 'media-db-igdb-client-id', - igdbClientSecret: 'media-db-igdb-client-secret', - rawg: 'media-db-rawg', - comicVine: 'media-db-comic-vine', - boardgameGeek: 'media-db-boardgame-geek', - genius: 'media-db-genius', -} as const; - -export type ApiSecretId = (typeof API_SECRET_IDS)[keyof typeof API_SECRET_IDS]; diff --git a/src/settings/apiSecretsHelper.ts b/src/settings/apiSecretsHelper.ts new file mode 100644 index 00000000..a8336c4f --- /dev/null +++ b/src/settings/apiSecretsHelper.ts @@ -0,0 +1,25 @@ +import type { App } from 'obsidian'; + +/** + * Settings slots for API credentials. Each slot stores the Obsidian SecretStorage **id** + * the user selects (including via SecretComponent → Link), not the raw secret. + * @see https://docs.obsidian.md/plugins/guides/secret-storage + */ +export enum ApiSecretID { + omdb, + tmdb, + mobyGames, + giantBomb, + igdbClientId, + igdbClientSecret, + rawg, + comicVine, + boardgameGeek, + genius, +} + +export function getApiSecretValue(app: App, linked: Record | undefined, slot: ApiSecretID): string { + const id = linked?.[slot] ?? ''; + if (id === '') return ''; + return app.secretStorage.getSecret(id) ?? ''; +} From 66d5cd462f7c62df30fc47976287f950265518ab Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 18:54:02 -0700 Subject: [PATCH 19/60] Fix lyrics extractor --- src/api/GeniusClient.ts | 38 ++------------- src/api/geniusLyricsExtract.ts | 87 ++++++++++++++++++++++++++++++++++ src/main.ts | 5 +- test/genius-lyrics.test.ts | 42 ++++++++++++++++ 4 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 src/api/geniusLyricsExtract.ts create mode 100644 test/genius-lyrics.test.ts diff --git a/src/api/GeniusClient.ts b/src/api/GeniusClient.ts index 154e9e5c..4b228177 100644 --- a/src/api/GeniusClient.ts +++ b/src/api/GeniusClient.ts @@ -1,6 +1,9 @@ import { requestUrl } from 'obsidian'; + import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; +import { extractLyricsFromGeniusHtml } from './geniusLyricsExtract'; + interface GeniusSearchHit { result: { id: number; @@ -16,40 +19,7 @@ interface GeniusSearchResponse { }; } -const LYRICS_DIV_RE = /]*data-lyrics-container="true"[^>]*>([\s\S]*?)<\/div>/gi; -const LYRICS_CLASS_FALLBACK_RE = /]*class="[^"]*Lyrics__Container[^"]*"[^>]*>([\s\S]*?)<\/div>/gi; - -function stripHtmlToPlainLyrics(fragment: string): string { - return fragment - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n') - .replace(/<[^>]+>/g, '') - .replace(/\n{3,}/g, '\n\n') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .trim(); -} - -function extractLyricsFromGeniusHtml(html: string): string { - const chunks: string[] = []; - let m: RegExpExecArray | null; - while ((m = LYRICS_DIV_RE.exec(html)) !== null) { - chunks.push(stripHtmlToPlainLyrics(m[1])); - } - LYRICS_DIV_RE.lastIndex = 0; - if (chunks.length > 0) { - return chunks.filter(Boolean).join('\n\n').trim(); - } - while ((m = LYRICS_CLASS_FALLBACK_RE.exec(html)) !== null) { - chunks.push(stripHtmlToPlainLyrics(m[1])); - } - LYRICS_CLASS_FALLBACK_RE.lastIndex = 0; - return chunks.filter(Boolean).join('\n\n').trim(); -} +export { extractLyricsFromGeniusHtml }; export class GeniusClient { private readonly accessToken: string | undefined; diff --git a/src/api/geniusLyricsExtract.ts b/src/api/geniusLyricsExtract.ts new file mode 100644 index 00000000..000e9218 --- /dev/null +++ b/src/api/geniusLyricsExtract.ts @@ -0,0 +1,87 @@ +const LYRICS_CONTAINER_OPEN_RE = + /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; + +function stripHtmlToPlainLyrics(fragment: string): string { + return fragment + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/\n{3,}/g, '\n\n') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); +} + +/** Parses nested
blocks; the naive `*?
` regex stops at the first inner close tag. */ +function extractBalancedDivInnerHtml(html: string, contentStart: number): string { + let depth = 1; + let i = contentStart; + const openRe = //gi; + while (depth > 0) { + openRe.lastIndex = i; + closeRe.lastIndex = i; + const om = openRe.exec(html); + const cm = closeRe.exec(html); + if (!cm) { + break; + } + const oIdx = om ? om.index : Number.POSITIVE_INFINITY; + const cIdx = cm.index; + if (om && oIdx < cIdx) { + depth++; + i = om.index + om[0].length; + } else { + depth--; + if (depth === 0) { + return html.slice(contentStart, cIdx); + } + i = cm.index + cm[0].length; + } + } + return ''; +} + +function collectLyricsContainersRegex(html: string): string[] { + const chunks: string[] = []; + let m: RegExpExecArray | null; + LYRICS_CONTAINER_OPEN_RE.lastIndex = 0; + while ((m = LYRICS_CONTAINER_OPEN_RE.exec(html)) !== null) { + const inner = extractBalancedDivInnerHtml(html, m.index + m[0].length); + if (inner) { + chunks.push(inner); + } + } + return chunks; +} + +function extractOneContainerPlain(el: Element): string { + const clone = el.cloneNode(true) as Element; + clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(node => node.remove()); + return stripHtmlToPlainLyrics(clone.innerHTML); +} + +export function extractLyricsFromGeniusHtml(html: string): string { + let chunks: string[] = []; + try { + const doc = new DOMParser().parseFromString(html, 'text/html'); + doc.querySelectorAll('[data-lyrics-container="true"]').forEach(c => { + const plain = extractOneContainerPlain(c); + if (plain) { + chunks.push(plain); + } + }); + } catch { + chunks = []; + } + + if (chunks.length === 0) { + return '' + } + + return chunks.join('\n\n').replace(/\n{3,}/g, '\n\n').trim(); +} diff --git a/src/main.ts b/src/main.ts index 6b9fc84e..1c600806 100644 --- a/src/main.ts +++ b/src/main.ts @@ -733,9 +733,8 @@ export default class MediaDbPlugin extends Plugin { if (mediaTypeModel.getMediaType() === MediaType.Song) { const song = mediaTypeModel as SongModel; - const body = (song.lyrics ?? '').trim(); - if(body.length > 0) { - fileContent += `# Lyrics\n\`\`\`\n${body}\n\`\`\`\n`; + if(song.lyrics.length > 0) { + fileContent += `# Lyrics\n\`\`\`\n${song.lyrics}\n\`\`\`\n`; } } diff --git a/test/genius-lyrics.test.ts b/test/genius-lyrics.test.ts new file mode 100644 index 00000000..d7b1c02e --- /dev/null +++ b/test/genius-lyrics.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from 'bun:test'; + +import { extractLyricsFromGeniusHtml } from '../src/api/geniusLyricsExtract'; + +describe('extractLyricsFromGeniusHtml', () => { + test('keeps all lines when lyrics use nested divs (balanced extraction)', () => { + const html = ` +
+

Line one

+

Line two nested

+

Line three

+
+ `; + const out = extractLyricsFromGeniusHtml(html); + expect(out).toContain('Line one'); + expect(out).toContain('Line two nested'); + expect(out).toContain('Line three'); + }); + + test('removes Genius 2024+ in-column header without breaking nested __Group / __Title classes', () => { + const html = `
+
+ +
+
+

Book of the Fallen Lyrics

+
+
+
+

First verse line
Second line

+
`; + const out = extractLyricsFromGeniusHtml(html); + expect(out).not.toMatch(/contributors/i); + expect(out).not.toContain('Book of the Fallen Lyrics'); + expect(out).toContain('First verse line'); + expect(out).toContain('Second line'); + }); +}); From dc4e756469d98696a4b793ff0a0fd94550960dc7 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 19:29:38 -0700 Subject: [PATCH 20/60] Extend title normalization --- src/main.ts | 2 +- src/settings/Settings.ts | 30 +++++++++++----------- src/utils/normalizeTitleForAlias.ts | 40 +++++++++++++++++++++++++---- test/normalizeTitleForAlias.test.ts | 29 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 test/normalizeTitleForAlias.test.ts diff --git a/src/main.ts b/src/main.ts index 1c600806..178aa4a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -127,7 +127,7 @@ export default class MediaDbPlugin extends Plugin { for (const mediaType of MEDIA_TYPES) { this.addCommand({ id: `open-media-db-search-modal-with-${mediaType}`, - name: `Create Media DB entry (${unCamelCase(mediaType)})`, + name: `Create Media DB entry: ${unCamelCase(mediaType)}`, callback: () => this.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), }); } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index dd358442..69c6f18f 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -812,21 +812,6 @@ export class MediaDbSettingTab extends PluginSettingTab { }), ); - generalGroup.addSetting( - setting => - void setting - .setName('Add Normalize Titles as Alias') - .setDesc( - 'When the title uses accented or special Latin letters (e.g. ó, ø), add an ASCII form to YAML aliases so links without those characters still resolve (e.g. Likbør → Likbor).', - ) - .addToggle(cb => { - cb.setValue(this.plugin.settings.addNormalizeTitlesAsAlias).onChange(data => { - this.plugin.settings.addNormalizeTitlesAsAlias = data; - void this.plugin.saveSettings(); - }); - }), - ); - generalGroup.addSetting( setting => void setting @@ -932,6 +917,21 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }), ); + + generalGroup.addSetting( + setting => + void setting + .setName('Add Normalized Titles as Alias') + .setDesc( + 'If the title contains non-ASCII characters, add a normalized ASCII version of the title in aliases.', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.addNormalizeTitlesAsAlias).onChange(data => { + this.plugin.settings.addNormalizeTitlesAsAlias = data; + void this.plugin.saveSettings(); + }); + }), + ); }); addTab('api-keys', 'API keys', 'key', panel => { diff --git a/src/utils/normalizeTitleForAlias.ts b/src/utils/normalizeTitleForAlias.ts index cdb4b477..fd229be0 100644 --- a/src/utils/normalizeTitleForAlias.ts +++ b/src/utils/normalizeTitleForAlias.ts @@ -1,7 +1,39 @@ /** * ASCII-style form of a title for use as an Obsidian `aliases` entry (e.g. Likbør → Likbor). * Returns null when the title should not get an extra alias (unchanged after normalization). + * + * NFKD + stripping marks handles most Latin letters; pairs below cover letters that do not + * decompose usefully (ligatures, stroke letters, eth/thorn, eng, Turkish dotless i, …). */ +const ASCII_ALIAS_FOLDS: readonly [string, string][] = [ + ['æ', 'ae'], + ['Æ', 'Ae'], + ['œ', 'oe'], + ['Œ', 'Oe'], + ['ø', 'o'], + ['Ø', 'O'], + ['ß', 'ss'], + ['ẞ', 'SS'], + ['ð', 'd'], + ['Ð', 'D'], + ['þ', 'th'], + ['Þ', 'Th'], + ['đ', 'd'], + ['Đ', 'D'], + ['ı', 'i'], + ['ħ', 'h'], + ['Ħ', 'H'], + ['ŋ', 'ng'], + ['Ŋ', 'Ng'], + ['Ł', 'L'], + ['ł', 'l'], + ['Ŀ', 'L'], + ['ŀ', 'l'], + ['ĸ', 'k'], + ['ʼn', "'n"], + ['ƿ', 'w'], +]; + export function normalizeTitleForAsciiAlias(title: string): string | null { const trimmed = title.trim(); if (!trimmed) { @@ -9,11 +41,9 @@ export function normalizeTitleForAsciiAlias(title: string): string | null { } let s = trimmed.normalize('NFKD').replace(/\p{M}/gu, ''); - s = s - .replaceAll('ø', 'o') - .replaceAll('Ø', 'O') - .replaceAll('ß', 'ss') - .replaceAll('ẞ', 'SS'); + for (const [from, to] of ASCII_ALIAS_FOLDS) { + s = s.replaceAll(from, to); + } if (s === trimmed) { return null; diff --git a/test/normalizeTitleForAlias.test.ts b/test/normalizeTitleForAlias.test.ts new file mode 100644 index 00000000..56d713c3 --- /dev/null +++ b/test/normalizeTitleForAlias.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'bun:test'; + +import { normalizeTitleForAsciiAlias } from '../src/utils/normalizeTitleForAlias'; + +describe('normalizeTitleForAsciiAlias', () => { + test('maps Polish ł and Ł to ASCII l and L', () => { + expect(normalizeTitleForAsciiAlias('Bełchatów')).toBe('Belchatow'); + expect(normalizeTitleForAsciiAlias('Łódź')).toBe('Lodz'); + }); + + test('maps ligatures æ œ and stroke / Nordic / German letters', () => { + expect(normalizeTitleForAsciiAlias('Rændering')).toBe('Raendering'); + expect(normalizeTitleForAsciiAlias('Œdipus')).toBe('Oedipus'); + expect(normalizeTitleForAsciiAlias('København')).toBe('Kobenhavn'); + expect(normalizeTitleForAsciiAlias('Straße')).toBe('Strasse'); + }); + + test('maps eth, thorn, d-bar, Turkish dotless i, eng, apostrophe-n', () => { + expect(normalizeTitleForAsciiAlias('Þór')).toBe('Thor'); + expect(normalizeTitleForAsciiAlias('Đặc')).toBe('Dac'); + expect(normalizeTitleForAsciiAlias('Bağcılar')).toBe('Bagcilar'); + expect(normalizeTitleForAsciiAlias('Eĸa')).toBe('Eka'); + expect(normalizeTitleForAsciiAlias('Muŋ')).toBe('Mung'); + }); + + test('returns null when input is already ASCII-equivalent after NFKD', () => { + expect(normalizeTitleForAsciiAlias('Plain Title')).toBe(null); + }); +}); From 8d26765eb132315a3ac3c269741212d7455d1c33 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 20:33:58 -0700 Subject: [PATCH 21/60] Filter out bootleg albums --- src/api/apis/MusicBrainzAPI.ts | 11 +++++++---- src/api/apis/MusicBrainzBandAPI.ts | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index 1d3abfdc..30b1fd9e 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -26,6 +26,10 @@ interface Release { status: string; } +function pickNonBootlegRelease(releases: Release[] | undefined): Release | undefined { + return releases?.find(r => r.status !== 'Bootlet'); +} + interface ArtistCredit { name: string; artist: { @@ -170,13 +174,12 @@ export class MusicBrainzAPI extends APIModel { const result = (await groupResponse.json) as IdResponse; - // Get ID of the first release - const firstRelease = result.releases?.[0]; + const firstRelease = pickNonBootlegRelease(result.releases); if (!firstRelease) { - throw Error('MDB | No releases found in release group.'); + throw Error('MDB | No non-bootleg release found in release group.'); } - // Fetch recordings for the first release + // Fetch recordings for the chosen release (skip MusicBrainz status=Bootleg when another edition exists) const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; console.log(`MDB | Fetching release recordings from: ${releaseUrl}`); diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzBandAPI.ts index eb936873..f831ba98 100644 --- a/src/api/apis/MusicBrainzBandAPI.ts +++ b/src/api/apis/MusicBrainzBandAPI.ts @@ -181,6 +181,8 @@ export class MusicBrainzBandAPI extends APIModel { /** * Lists release group MBIDs for studio albums (primary type album, excluding live/compilations/etc.). + * Passes release-group-status=website-default so MusicBrainz omits groups that only have bootleg, promotional, or pseudo-releases + * (see MusicBrainz API “Release (Group) Type and Status”). */ async listStudioAlbumReleaseGroupIds(artistId: string): Promise { const collected: { id: string; date: string }[] = []; @@ -189,7 +191,7 @@ export class MusicBrainzBandAPI extends APIModel { while (true) { await this.throttleMs(1100); - const url = `https://musicbrainz.org/ws/2/release-group?artist=${encodeURIComponent(artistId)}&type=album&fmt=json&limit=${limit}&offset=${offset}`; + const url = `https://musicbrainz.org/ws/2/release-group?artist=${encodeURIComponent(artistId)}&type=album&fmt=json&limit=${limit}&offset=${offset}&release-group-status=website-default`; const res = await requestUrl({ url, From 10599bebed3924fc2f63e147196aabb432a02bff Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 20:42:05 -0700 Subject: [PATCH 22/60] Add ISNI metadata field for bands --- src/api/apis/MusicBrainzBandAPI.ts | 11 +++++++++++ src/models/BandModel.ts | 3 +++ src/settings/PropertyMapper.ts | 1 + 3 files changed, 15 insertions(+) diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzBandAPI.ts index f831ba98..b93ad38a 100644 --- a/src/api/apis/MusicBrainzBandAPI.ts +++ b/src/api/apis/MusicBrainzBandAPI.ts @@ -22,6 +22,7 @@ interface ArtistSearchArtist { 'life-span'?: { begin?: string; end?: string }; country?: string; disambiguation?: string; + isnis?: string[]; } interface ArtistSearchResponse { @@ -35,6 +36,7 @@ interface ArtistDetailResponse { 'life-span'?: { begin?: string; end?: string }; country?: string; disambiguation?: string; + isnis?: string[]; tags?: ArtistTag[]; genres?: ArtistGenre[]; relations?: { url?: { resource: string } | null; type: string }[]; @@ -52,6 +54,13 @@ interface ReleaseGroupBrowseResponse { 'release-groups': ReleaseGroupListItem[]; } +function isniFromMusicBrainz(isnis: string[] | undefined): string { + if (!isnis?.length) { + return ''; + } + return isnis.join(', '); +} + const EXCLUDED_SECONDARY_TYPES = new Set([ 'Compilation', 'Live', @@ -121,6 +130,7 @@ export class MusicBrainzBandAPI extends APIModel { id: artist.id, country: artist.country ?? '', disambiguation: artist.disambiguation ?? '', + isni: isniFromMusicBrainz(artist.isnis), genres: [], image: '', officialWebsite: '', @@ -169,6 +179,7 @@ export class MusicBrainzBandAPI extends APIModel { id: artist.id, country: artist.country ?? '', disambiguation: artist.disambiguation ?? '', + isni: isniFromMusicBrainz(artist.isnis), genres: [...new Set([...(artist.genres?.map(g => g.name) ?? []), ...(artist.tags?.map(t => t.name) ?? [])])], image: '', officialWebsite, diff --git a/src/models/BandModel.ts b/src/models/BandModel.ts index e5ec1a24..5b09d1d5 100644 --- a/src/models/BandModel.ts +++ b/src/models/BandModel.ts @@ -11,6 +11,8 @@ export class BandModel extends MediaTypeModel { image: string; officialWebsite: string; disambiguation: string; + /** ISNI(s) from the data source; comma-separated if multiple. */ + isni: string; beginYear: string; releaseDate: string; @@ -26,6 +28,7 @@ export class BandModel extends MediaTypeModel { this.image = ''; this.officialWebsite = ''; this.disambiguation = ''; + this.isni = ''; this.beginYear = ''; this.releaseDate = ''; diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index a3c594bc..9477412f 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -214,6 +214,7 @@ export class PropertyMapper { id: '', country: '', disambiguation: '', + isni: '', genres: [], image: '', officialWebsite: '', From e7da03eb770cda65462cd039f750ba9fe38f3720 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 21:44:38 -0700 Subject: [PATCH 23/60] Added spotify link to song metadata --- src/api/SpotifyClient.ts | 145 +++++++++++++++++++++++++++++++ src/api/apis/MusicBrainzAPI.ts | 38 +++++++- src/main.ts | 25 ++++++ src/models/MusicReleaseModel.ts | 2 + src/models/SongModel.ts | 4 + src/settings/Settings.ts | 14 +++ src/settings/apiSecretsHelper.ts | 3 + 7 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/api/SpotifyClient.ts diff --git a/src/api/SpotifyClient.ts b/src/api/SpotifyClient.ts new file mode 100644 index 00000000..48e87bae --- /dev/null +++ b/src/api/SpotifyClient.ts @@ -0,0 +1,145 @@ +import { requestUrl } from 'obsidian'; + +import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; + +interface SpotifyTokenResponse { + access_token: string; + expires_in: number; + token_type: string; +} + +interface SpotifySearchResponse { + tracks?: { + items: { external_urls?: { spotify?: string } }[]; + }; +} + +function spotifyTrackArtistQuery(trackTitle: string, artistName: string): string { + const clean = (s: string) => s.trim().replace(/"/g, ' ').replace(/\s+/g, ' '); + const t = clean(trackTitle); + const a = clean(artistName); + if (!t) { + return ''; + } + if (!a) { + return `track:"${t}"`; + } + return `track:"${t}" artist:"${a}"`; +} + +export class SpotifyClient { + private readonly clientId: string; + private readonly clientSecret: string; + private readonly userAgent: string; + private accessToken: string | null = null; + private tokenExpiresAtMs = 0; + + constructor(clientId: string | undefined, clientSecret: string | undefined) { + this.clientId = (clientId ?? '').trim(); + this.clientSecret = (clientSecret ?? '').trim(); + this.userAgent = `${pluginName}/${mediaDbVersion} (${contactEmail})`; + } + + isConfigured(): boolean { + return Boolean(this.clientId && this.clientSecret); + } + + private async refreshAccessToken(): Promise { + if (!this.isConfigured()) { + return null; + } + const basic = btoa(`${this.clientId}:${this.clientSecret}`); + const res = await requestUrl({ + url: 'https://accounts.spotify.com/api/token', + method: 'POST', + throw: false, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basic}`, + 'User-Agent': this.userAgent, + }, + body: 'grant_type=client_credentials', + }); + if (res.status !== 200) { + console.warn(`MDB | Spotify token request returned ${res.status}`); + this.accessToken = null; + this.tokenExpiresAtMs = 0; + return null; + } + const data = res.json as SpotifyTokenResponse; + if (!data.access_token) { + return null; + } + this.accessToken = data.access_token; + const ttlMs = (data.expires_in ?? 3600) * 1000; + this.tokenExpiresAtMs = Date.now() + ttlMs - 60_000; + return this.accessToken; + } + + private async getAccessToken(): Promise { + if (!this.isConfigured()) { + return null; + } + const now = Date.now(); + if (this.accessToken && now < this.tokenExpiresAtMs) { + return this.accessToken; + } + return this.refreshAccessToken(); + } + + /** + * Search for a track and return the first result's open.spotify.com URL, or ''. + */ + async searchFirstTrackUrl(trackTitle: string, artistName: string): Promise { + const q = spotifyTrackArtistQuery(trackTitle, artistName); + if (!q) { + return ''; + } + let token = await this.getAccessToken(); + if (!token) { + console.warn('MDB | Spotify search fetch skipped: could not obtain access token'); + return ''; + } + + const params = new URLSearchParams({ q, type: 'track', limit: '1' }); + const url = `https://api.spotify.com/v1/search?${params.toString()}`; + console.log(`MDB | Spotify search fetch: ${url}`); + let res = await requestUrl({ + url, + method: 'GET', + throw: false, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': this.userAgent, + }, + }); + + if (res.status === 401) { + this.accessToken = null; + this.tokenExpiresAtMs = 0; + token = await this.refreshAccessToken(); + if (!token) { + return ''; + } + console.log(`MDB | Spotify search fetch (retry after 401): ${url}`); + res = await requestUrl({ + url, + method: 'GET', + throw: false, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': this.userAgent, + }, + }); + } + + if (res.status !== 200) { + console.warn(`MDB | Spotify search returned ${res.status}`); + return ''; + } + + const data = res.json as SpotifySearchResponse; + const link = data.tracks?.items?.[0]?.external_urls?.spotify; + return typeof link === 'string' ? link : ''; + } +} diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index 30b1fd9e..76bb4b67 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -83,6 +83,7 @@ interface MediaResponse { position: number; title: string; recording: { + id?: string; length: number; title: string; }; @@ -237,6 +238,39 @@ export class MusicBrainzAPI extends APIModel { getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; } + + /** + * Loads MusicBrainz recording URL relations and returns the open.spotify.com track URL if present. + * Callers should throttle requests (~1/s) per MusicBrainz etiquette. + */ + async fetchSpotifyUrlForRecording(recordingId: string): Promise { + if (!recordingId) { + return ''; + } + const recordingUrl = `https://musicbrainz.org/ws/2/recording/${encodeURIComponent(recordingId)}?inc=url-rels&fmt=json`; + const fetchData = await requestUrl({ + url: recordingUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }); + if (fetchData.status !== 200) { + console.warn(`MDB | Recording ${recordingId} url-rels returned ${fetchData.status}`); + return ''; + } + const data = (await fetchData.json) as RecordingUrlRelsResponse; + for (const rel of data.relations ?? []) { + const resource = rel.url?.resource; + if (typeof resource === 'string' && resource.includes('open.spotify.com')) { + return resource; + } + } + return ''; + } +} + +interface RecordingUrlRelsResponse { + relations?: { type: string; url?: { resource: string } }[]; } function extractTracksFromMedia(media: MediaResponse['media']): { @@ -247,17 +281,19 @@ function extractTracksFromMedia(media: MediaResponse['media']): { }[] { if (!media || media.length === 0 || !media[0].tracks) return []; - return media[0].tracks.map((track, index) => { + return media[0].tracks.map((track, index) => { const title = track.title ?? track.recording?.title ?? 'Unknown Title'; const rawLength = track.length ?? track.recording?.length; const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; + const recordingId = track.recording?.id; return { number: index + 1, title, duration, featuredArtists, + ...(recordingId ? { recordingId } : {}), }; }); } diff --git a/src/main.ts b/src/main.ts index 178aa4a3..1e4560c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,6 +23,7 @@ import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; import { VNDBAPI } from './api/apis/VNDBAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { GeniusClient } from './api/GeniusClient'; +import { SpotifyClient } from './api/SpotifyClient'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; @@ -529,6 +530,10 @@ export default class MediaDbPlugin extends Plugin { new Notice('Band import: add a Genius API access token in settings to fetch lyrics.'); } + const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; + const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; + const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); + const bandApi = this.apiManager.getApiByName('MusicBrainz Band API') as MusicBrainzBandAPI | undefined; const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; if (!bandApi || !musicBrainzApi) { @@ -600,6 +605,25 @@ export default class MediaDbPlugin extends Plugin { } } + let spotifyUrl = ''; + if (track.recordingId) { + await new Promise(r => setTimeout(r, 1100)); + try { + spotifyUrl = await musicBrainzApi.fetchSpotifyUrlForRecording(track.recordingId); + } catch (e) { + console.warn(`MDB | Spotify URL for recording ${track.recordingId}:`, e); + } + } + if (!spotifyUrl && spotify.isConfigured()) { + const primaryArtist = release.artists[0] ?? band.title; + console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); + try { + spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); + } catch (e) { + console.warn(`MDB | Spotify search for "${track.title}":`, e); + } + } + const song = new SongModel({ type: 'song', title: track.title, @@ -619,6 +643,7 @@ export default class MediaDbPlugin extends Plugin { duration: track.duration, featuredArtists: track.featuredArtists, geniusUrl, + spotifyUrl, lyrics, userData: { personalRating: 0 }, }); diff --git a/src/models/MusicReleaseModel.ts b/src/models/MusicReleaseModel.ts index 38427a91..d5e236bd 100644 --- a/src/models/MusicReleaseModel.ts +++ b/src/models/MusicReleaseModel.ts @@ -19,6 +19,8 @@ export class MusicReleaseModel extends MediaTypeModel { title: string; duration: string; featuredArtists: string[]; + /** MusicBrainz recording MBID; used to resolve Spotify and other links. */ + recordingId?: string; }[]; userData: { diff --git a/src/models/SongModel.ts b/src/models/SongModel.ts index b57fb7bf..6c45c8aa 100644 --- a/src/models/SongModel.ts +++ b/src/models/SongModel.ts @@ -14,6 +14,8 @@ export class SongModel extends MediaTypeModel { duration: string; featuredArtists: string[]; geniusUrl: string; + /** Open track URL from MusicBrainz (e.g. https://open.spotify.com/track/…) when available. */ + spotifyUrl: string; lyrics: string; image: string; releaseDate: string; @@ -33,6 +35,7 @@ export class SongModel extends MediaTypeModel { this.duration = ''; this.featuredArtists = []; this.geniusUrl = ''; + this.spotifyUrl = ''; this.lyrics = ''; this.image = ''; this.releaseDate = ''; @@ -54,6 +57,7 @@ export class SongModel extends MediaTypeModel { this.duration = obj.duration ?? ''; this.featuredArtists = obj.featuredArtists ?? []; this.geniusUrl = obj.geniusUrl ?? ''; + this.spotifyUrl = obj.spotifyUrl ?? ''; this.lyrics = obj.lyrics ?? ''; this.releaseDate = obj.releaseDate ?? ''; } diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 69c6f18f..e74d4234 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -441,6 +441,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { [ApiSecretID.comicVine]: '', [ApiSecretID.boardgameGeek]: '', [ApiSecretID.genius]: '', + [ApiSecretID.spotifyClientId]: '', + [ApiSecretID.spotifyClientSecret]: '', }, }; @@ -952,6 +954,18 @@ export class MediaDbSettingTab extends PluginSettingTab { 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing a band.', ApiSecretID.genius, ); + this.addApiSecretSetting( + apiKeyGroup, + 'Spotify Client ID', + 'From https://developer.spotify.com/dashboard — used to resolve track links when MusicBrainz has no Spotify URL (with Client Secret).', + ApiSecretID.spotifyClientId, + ); + this.addApiSecretSetting( + apiKeyGroup, + 'Spotify Client Secret', + 'Pair with Spotify Client ID for client-credentials access to search tracks during band import.', + ApiSecretID.spotifyClientSecret, + ); }); let musicTabAdded = false; diff --git a/src/settings/apiSecretsHelper.ts b/src/settings/apiSecretsHelper.ts index a8336c4f..b20908b7 100644 --- a/src/settings/apiSecretsHelper.ts +++ b/src/settings/apiSecretsHelper.ts @@ -16,6 +16,9 @@ export enum ApiSecretID { comicVine, boardgameGeek, genius, + /** Spotify Developer Dashboard — used when MusicBrainz has no streaming URL for a recording. */ + spotifyClientId, + spotifyClientSecret, } export function getApiSecretValue(app: App, linked: Record | undefined, slot: ApiSecretID): string { From 613c37961fd7c8774b883c6c3361d3b4a8ca0e54 Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Mon, 30 Mar 2026 22:20:44 -0700 Subject: [PATCH 24/60] Make duration int, add budget and revenue --- src/api/apis/MALAPI.ts | 6 +++--- src/api/apis/OMDbAPI.ts | 6 +++--- src/api/apis/TMDBMovieAPI.ts | 42 ++++++++++++++++++++++++++++++++---- src/models/MovieModel.ts | 16 +++++++++----- src/utils/Utils.ts | 40 ++++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 15 deletions(-) diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index cfb4acea..fc0c6986 100644 --- a/src/api/apis/MALAPI.ts +++ b/src/api/apis/MALAPI.ts @@ -1,5 +1,5 @@ import createClient from 'openapi-fetch'; -import { coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; +import { coerceMovieDurationMinutes, coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; @@ -141,7 +141,7 @@ export class MALAPI extends APIModel { plot: result.synopsis, genres: result.genres?.map(x => x.name).filter(isTruthy), studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, + duration: coerceMovieDurationMinutes(result.duration), onlineRating: result.score, image: result.images?.jpg?.image_url, @@ -172,7 +172,7 @@ export class MALAPI extends APIModel { plot: result.synopsis, genres: result.genres?.map(x => x.name).filter(isTruthy), studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: result.duration, + duration: coerceMovieDurationMinutes(result.duration), onlineRating: result.score, image: result.images?.jpg?.image_url, diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index a96ec4b1..28d69242 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -6,7 +6,7 @@ import { MovieModel } from '../../models/MovieModel'; import { SeriesModel } from '../../models/SeriesModel'; import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; -import { coerceYear } from '../../utils/Utils'; +import { coerceMovieDurationMinutes, coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; interface ErrorResponse { @@ -210,14 +210,14 @@ export class OMDbAPI extends APIModel { genres: result.Genre?.split(', '), director: result.Director?.split(', '), writer: result.Writer?.split(', '), - duration: result.Runtime, + duration: coerceMovieDurationMinutes(result.Runtime), onlineRating: Number.parseFloat(result.imdbRating ?? 0), actors: result.Actors?.split(', '), image: result.Poster.replace('_SX300', '_SX600'), released: true, country: result.Country?.split(', '), - boxOffice: result.BoxOffice, + revenue: result.BoxOffice && result.BoxOffice !== 'N/A' ? result.BoxOffice : '', ageRating: result.Rated, premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 0444ea0c..0f2e40dd 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -3,12 +3,45 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MovieModel } from '../../models/MovieModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; +import { formatUsdWholeDollars } from '../../utils/Utils'; import { APIModel } from '../APIModel'; import type { paths } from '../schemas/TMDB'; +/** TMDB `credits.crew` jobs that count as writing credits for movies. */ +const TMDB_WRITING_CREW_JOBS = new Set([ + 'Writer', + 'Screenplay', + 'Story', + 'Teleplay', + 'Original Story', + 'Characters', + 'Novel', + 'Screenstory', +]); + +function tmdbWritingCreditsFromCrew(crew: any[] | undefined): string[] { + if (!crew?.length) { + return []; + } + const seen = new Set(); + const names: string[] = []; + for (const c of crew) { + const job = c?.job as string | undefined; + const name = c?.name as string | undefined; + if (!job || !name || !TMDB_WRITING_CREW_JOBS.has(job)) { + continue; + } + if (!seen.has(name)) { + seen.add(name); + names.push(name); + } + } + return names; +} + export class TMDBMovieAPI extends APIModel { plugin: MediaDbPlugin; typeMappings: Map; @@ -134,19 +167,20 @@ export class TMDBMovieAPI extends APIModel { plot: result.overview ?? '', genres: result.genres?.map((g: any) => g.name) ?? [], // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type - // @ts-ignore - writer: result.credits.crew?.filter((c: any) => c.job === 'Screenplay').map((c: any) => c.name) ?? [], + writer: tmdbWritingCreditsFromCrew((result as { credits?: { crew?: any[] } }).credits?.crew), // @ts-ignore 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?.toString() ?? 'unknown', + duration: result.runtime != null && Number.isFinite(result.runtime) ? Math.trunc(result.runtime) : 0, onlineRating: result.vote_average, // @ts-ignore 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!), + budget: formatUsdWholeDollars(result.budget ?? 0), + revenue: formatUsdWholeDollars(result.revenue ?? 0), streamingServices: [], userData: { diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index 563a0f98..a0dcaf91 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -1,6 +1,6 @@ import { MediaType } from '../utils/MediaType'; import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { coerceMovieDurationMinutes, mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaTypeModel } from './MediaTypeModel'; export type MovieData = ModelToData; @@ -12,14 +12,18 @@ export class MovieModel extends MediaTypeModel { director: string[]; writer: string[]; studio: string[]; - duration: string; + /** Total runtime in minutes. */ + duration: number; onlineRating: number; actors: string[]; image: string; released: boolean; country: string[]; - boxOffice: string; + /** Production budget in USD (e.g. from TMDB). */ + budget: string; + /** Box-office gross (e.g. worldwide from TMDB; OMDb US figure when from IMDb). */ + revenue: string; ageRating: string; streamingServices: string[]; premiere: string; @@ -39,14 +43,15 @@ export class MovieModel extends MediaTypeModel { this.director = []; this.writer = []; this.studio = []; - this.duration = ''; + this.duration = 0; this.onlineRating = 0; this.actors = []; this.image = ''; this.released = false; this.country = []; - this.boxOffice = ''; + this.budget = ''; + this.revenue = ''; this.ageRating = ''; this.streamingServices = []; this.premiere = ''; @@ -58,6 +63,7 @@ export class MovieModel extends MediaTypeModel { }; migrateObject(this, obj, this); + this.duration = coerceMovieDurationMinutes(this.duration as unknown); if (!Object.hasOwn(obj, 'userData')) { migrateObject(this.userData, obj, this.userData); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index c8eda428..20961dd1 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -204,6 +204,38 @@ export interface CreateNoteOptions { folder?: TFolder; } +/** Runtime in whole minutes (TMDB/OMDb/MAL). 0 when unknown. Parses legacy string frontmatter (e.g. "136 min", "2 hr 5 min"). */ +export function coerceMovieDurationMinutes(value: unknown): number { + if (value === undefined || value === null) { + return 0; + } + if (typeof value === 'number') { + const n = Math.trunc(value); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + if (typeof value === 'string') { + const t = value.trim(); + if (t === '' || t.toLowerCase() === 'unknown' || t.toUpperCase() === 'N/A' || t === 'TBA') { + return 0; + } + let total = 0; + const hours = t.match(/(\d+)\s*(?:hours?|hrs?)\b/i) ?? t.match(/(\d+)\s*h\b/i); + const mins = t.match(/(\d+)\s*(?:minutes?|mins?)\b/i) ?? t.match(/(\d+)\s*min\b/i); + if (hours) { + total += parseInt(hours[1], 10) * 60; + } + if (mins) { + total += parseInt(mins[1], 10); + } + if (total > 0) { + return total; + } + const n = parseInt(t, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + return 0; +} + /** Normalizes release year for metadata: integer, 0 when unknown or non-numeric. */ export function coerceYear(value: unknown): number { if (value === undefined || value === null) return 0; @@ -279,6 +311,14 @@ export async function useTemplaterPluginInFile(app: App, file: TFile): Promise = { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type [K in keyof T as T[K] extends Function ? never : K]?: T[K] | null; From 313906a283b05101328adc8bb740d8a852b6450b Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Tue, 31 Mar 2026 23:49:23 -0700 Subject: [PATCH 25/60] Fix bug where cancelling an overwrite for an album would continue fetching songlist for it --- src/main.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 1e4560c3..0174c45f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -481,7 +481,8 @@ export default class MediaDbPlugin extends Plugin { await this.createStandardMediaDbNoteFromModel(mediaTypeModel, options); } - private async createStandardMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + /** @returns whether the note file was created (false if the user cancelled overwrite or an error occurred before the file was written). */ + private async createStandardMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { try { console.debug('MDB | creating new note'); @@ -498,11 +499,18 @@ export default class MediaDbPlugin extends Plugin { const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); if (this.settings.enableTemplaterIntegration) { - await useTemplaterPluginInFile(this.app, targetFile); + try { + await useTemplaterPluginInFile(this.app, targetFile); + } catch (e) { + console.warn(e); + new Notice(`${e}`); + } } + return true; } catch (e) { console.warn(e); new Notice(`${e}`); + return false; } } @@ -559,7 +567,10 @@ export default class MediaDbPlugin extends Plugin { albumNotesFolder = await this.ensureVaultFolder(treeRootPath); } - await this.createStandardMediaDbNoteFromModel(band, { ...options, folder: bandNoteFolder }); + const bandNoteCreated = await this.createStandardMediaDbNoteFromModel(band, { ...options, folder: bandNoteFolder }); + if (!bandNoteCreated) { + return; + } let releaseGroupIds: string[]; try { @@ -590,7 +601,10 @@ export default class MediaDbPlugin extends Plugin { const releaseOpts: CreateNoteOptions = useTree ? { ...childOptions, folder: albumNotesFolder } : { ...childOptions }; - await this.createStandardMediaDbNoteFromModel(release, releaseOpts); + const albumNoteCreated = await this.createStandardMediaDbNoteFromModel(release, releaseOpts); + if (!albumNoteCreated) { + continue; + } for (const track of release.tracks) { let lyrics = ''; From 39613f999fd75d36cc8b621a1fb04d54f926ae8f Mon Sep 17 00:00:00 2001 From: Darren Sadr Date: Tue, 31 Mar 2026 23:52:24 -0700 Subject: [PATCH 26/60] Rename Band to Artist --- src/api/APIManager.ts | 2 +- ...ainzBandAPI.ts => MusicBrainzArtistAPI.ts} | 22 +++--- src/api/musicBrainzConstants.ts | 8 +-- src/main.ts | 53 +++++++------- src/models/{BandModel.ts => ArtistModel.ts} | 10 +-- src/settings/PropertyMapper.ts | 24 +++---- src/settings/PropertyMapping.ts | 10 ++- src/settings/Settings.ts | 70 +++++++++---------- src/utils/MediaType.ts | 2 +- src/utils/MediaTypeManager.ts | 14 ++-- src/utils/noteTypeSettings.ts | 5 +- 11 files changed, 115 insertions(+), 105 deletions(-) rename src/api/apis/{MusicBrainzBandAPI.ts => MusicBrainzArtistAPI.ts} (94%) rename src/models/{BandModel.ts => ArtistModel.ts} (85%) diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 0fdf8a07..a244ce26 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -51,7 +51,7 @@ export class APIManager { /** * Queries detailed info for an id from an API. - * MusicBrainz-backed notes use on-disk dataSource `MusicBrainz`; `mediaType` picks Band vs release/song API. + * MusicBrainz-backed notes use on-disk dataSource `MusicBrainz`; `mediaType` picks artist vs release/song API. * * @param id * @param apiName Stored dataSource on the note, or an exact {@link APIModel.apiName} (e.g. bulk import / ID search). diff --git a/src/api/apis/MusicBrainzBandAPI.ts b/src/api/apis/MusicBrainzArtistAPI.ts similarity index 94% rename from src/api/apis/MusicBrainzBandAPI.ts rename to src/api/apis/MusicBrainzArtistAPI.ts index b93ad38a..397b6c81 100644 --- a/src/api/apis/MusicBrainzBandAPI.ts +++ b/src/api/apis/MusicBrainzArtistAPI.ts @@ -1,6 +1,6 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; -import { BandModel } from '../../models/BandModel'; +import { ArtistModel } from '../../models/ArtistModel'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; import { MediaType } from '../../utils/MediaType'; import { coerceYear, contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; @@ -75,7 +75,7 @@ const EXCLUDED_SECONDARY_TYPES = new Set([ 'Field recording', ]); -export class MusicBrainzBandAPI extends APIModel { +export class MusicBrainzArtistAPI extends APIModel { plugin: MediaDbPlugin; apiDateFormat: string = 'YYYY-MM-DD'; @@ -83,10 +83,10 @@ export class MusicBrainzBandAPI extends APIModel { super(); this.plugin = plugin; - this.apiName = 'MusicBrainz Band API'; + this.apiName = 'MusicBrainz Artist API'; this.apiDescription = 'MusicBrainz artist search and studio album discography.'; this.apiUrl = 'https://musicbrainz.org/'; - this.types = [MediaType.Band]; + this.types = [MediaType.Artist]; } private mbHeaders(): Record { @@ -118,8 +118,8 @@ export class MusicBrainzBandAPI extends APIModel { for (const artist of data.artists ?? []) { const begin = artist['life-span']?.begin; ret.push( - new BandModel({ - type: 'band', + new ArtistModel({ + type: 'artist', title: artist.name, englishTitle: artist.name, year: coerceYear(begin ? (begin.split('-')[0] ?? '') : ''), @@ -134,7 +134,7 @@ export class MusicBrainzBandAPI extends APIModel { genres: [], image: '', officialWebsite: '', - subType: 'band', + subType: 'artist', }), ); } @@ -167,8 +167,8 @@ export class MusicBrainzBandAPI extends APIModel { } } - return new BandModel({ - type: 'band', + return new ArtistModel({ + type: 'artist', title: artist.name, englishTitle: artist.name, year: coerceYear(beginYear), @@ -183,7 +183,7 @@ export class MusicBrainzBandAPI extends APIModel { genres: [...new Set([...(artist.genres?.map(g => g.name) ?? []), ...(artist.tags?.map(t => t.name) ?? [])])], image: '', officialWebsite, - subType: 'band', + subType: 'artist', userData: { personalRating: 0, }, @@ -244,6 +244,6 @@ export class MusicBrainzBandAPI extends APIModel { } getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MusicBrainzBandAPI_disabledMediaTypes; + return this.plugin.settings.MusicBrainzArtistAPI_disabledMediaTypes; } } diff --git a/src/api/musicBrainzConstants.ts b/src/api/musicBrainzConstants.ts index 6ad4c27f..bb218884 100644 --- a/src/api/musicBrainzConstants.ts +++ b/src/api/musicBrainzConstants.ts @@ -1,6 +1,6 @@ import { MediaType } from '../utils/MediaType'; -/** Stored on notes for any row backed by MusicBrainz (release, band, or song). */ +/** Stored on notes for any row backed by MusicBrainz (release, artist, or song). */ export const MUSICBRAINZ_NOTE_DATA_SOURCE = 'MusicBrainz'; export function isMusicBrainzFamilyDataSource(dataSource: string): boolean { @@ -8,9 +8,9 @@ export function isMusicBrainzFamilyDataSource(dataSource: string): boolean { } /** Which registered API implements getById for this media type. */ -export function musicBrainzRegisteredApiName(mediaType: MediaType): 'MusicBrainz API' | 'MusicBrainz Band API' | undefined { - if (mediaType === MediaType.Band) { - return 'MusicBrainz Band API'; +export function musicBrainzRegisteredApiName(mediaType: MediaType): 'MusicBrainz API' | 'MusicBrainz Artist API' | undefined { + if (mediaType === MediaType.Artist) { + return 'MusicBrainz Artist API'; } if (mediaType === MediaType.MusicRelease || mediaType === MediaType.Song) { return 'MusicBrainz API'; diff --git a/src/main.ts b/src/main.ts index 0174c45f..81a752e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ import { MALAPI } from './api/apis/MALAPI'; import { MALAPIManga } from './api/apis/MALAPIManga'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; -import { MusicBrainzBandAPI } from './api/apis/MusicBrainzBandAPI'; +import { MusicBrainzArtistAPI } from './api/apis/MusicBrainzArtistAPI'; import { OMDbAPI } from './api/apis/OMDbAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { RAWGAPI } from './api/apis/RAWGAPI'; @@ -27,7 +27,7 @@ import { SpotifyClient } from './api/SpotifyClient'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; -import type { BandModel } from './models/BandModel'; +import type { ArtistModel } from './models/ArtistModel'; import type { MediaTypeModel } from './models/MediaTypeModel'; import type { MusicReleaseModel } from './models/MusicReleaseModel'; import type { SeasonModel } from './models/SeasonModel'; @@ -36,7 +36,7 @@ import { ApiSecretID, getApiSecretValue } from './settings/apiSecretsHelper'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; -import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOrder } from './settings/Settings'; +import { getDefaultSettings, MediaDbSettingTab, migrateLoadedPluginSettings, propertyMappingModelsInDisplayOrder } from './settings/Settings'; import { BulkImportHelper } from './utils/BulkImportHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; @@ -75,7 +75,7 @@ export default class MediaDbPlugin extends Plugin { this.apiManager.registerAPI(new MALAPIManga(this)); this.apiManager.registerAPI(new WikipediaAPI(this)); this.apiManager.registerAPI(new MusicBrainzAPI(this)); - this.apiManager.registerAPI(new MusicBrainzBandAPI(this)); + this.apiManager.registerAPI(new MusicBrainzArtistAPI(this)); this.apiManager.registerAPI(new SteamAPI(this)); this.apiManager.registerAPI(new TMDBSeriesAPI(this)); this.apiManager.registerAPI(new TMDBSeasonAPI(this)); @@ -436,9 +436,9 @@ export default class MediaDbPlugin extends Plugin { } async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - const hasBand = models.some(m => m.getMediaType() === MediaType.Band); + const hasArtist = models.some(m => m.getMediaType() === MediaType.Artist); - if (hasBand) { + if (hasArtist) { for (const model of models) { await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); } @@ -473,8 +473,8 @@ export default class MediaDbPlugin extends Plugin { } async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - if (mediaTypeModel.getMediaType() === MediaType.Band) { - await this.importBandDiscography(mediaTypeModel as BandModel, options); + if (mediaTypeModel.getMediaType() === MediaType.Artist) { + await this.importArtistDiscography(mediaTypeModel as ArtistModel, options); return; } @@ -530,26 +530,26 @@ export default class MediaDbPlugin extends Plugin { return folder; } - private async importBandDiscography(band: BandModel, options: CreateNoteOptions): Promise { + private async importArtistDiscography(artist: ArtistModel, options: CreateNoteOptions): Promise { try { const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; const genius = new GeniusClient(geniusToken); if (!genius.isConfigured()) { - new Notice('Band import: add a Genius API access token in settings to fetch lyrics.'); + new Notice('Artist import: add a Genius API access token in settings to fetch lyrics.'); } const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); - const bandApi = this.apiManager.getApiByName('MusicBrainz Band API') as MusicBrainzBandAPI | undefined; + const artistApi = this.apiManager.getApiByName('MusicBrainz Artist API') as MusicBrainzArtistAPI | undefined; const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; - if (!bandApi || !musicBrainzApi) { + if (!artistApi || !musicBrainzApi) { new Notice('MusicBrainz APIs not available.'); return; } - const useTree = this.settings.bandUseFileTreeForSongs; + const useTree = this.settings.artistUseFileTreeForSongs; const childOptions: CreateNoteOptions = { attachTemplate: true, openNote: false, @@ -557,30 +557,30 @@ export default class MediaDbPlugin extends Plugin { folder: undefined, }; - const bandBaseFolder = await this.mediaTypeManager.getFolder(band, this.app); - let bandNoteFolder = bandBaseFolder; - let albumNotesFolder = bandBaseFolder; + const artistBaseFolder = await this.mediaTypeManager.getFolder(artist, this.app); + let artistNoteFolder = artistBaseFolder; + let albumNotesFolder = artistBaseFolder; if (useTree) { - const bandSeg = this.safeFileTreeSegment(band.title); - const treeRootPath = normalizePath(`${bandBaseFolder.path}/${bandSeg}`); + const artistSeg = this.safeFileTreeSegment(artist.title); + const treeRootPath = normalizePath(`${artistBaseFolder.path}/${artistSeg}`); albumNotesFolder = await this.ensureVaultFolder(treeRootPath); } - const bandNoteCreated = await this.createStandardMediaDbNoteFromModel(band, { ...options, folder: bandNoteFolder }); - if (!bandNoteCreated) { + const artistNoteCreated = await this.createStandardMediaDbNoteFromModel(artist, { ...options, folder: artistNoteFolder }); + if (!artistNoteCreated) { return; } let releaseGroupIds: string[]; try { - releaseGroupIds = await bandApi.listStudioAlbumReleaseGroupIds(band.id); + releaseGroupIds = await artistApi.listStudioAlbumReleaseGroupIds(artist.id); } catch (e) { new Notice(`Could not load albums: ${e}`); return; } - new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${band.title}…`); + new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${artist.title}…`); for (const rgId of releaseGroupIds) { await new Promise(r => setTimeout(r, 1100)); @@ -611,7 +611,7 @@ export default class MediaDbPlugin extends Plugin { let geniusUrl = ''; if (genius.isConfigured()) { await new Promise(r => setTimeout(r, 500)); - const hit = await genius.searchFirstSongHit(`${band.title} ${track.title}`); + const hit = await genius.searchFirstSongHit(`${artist.title} ${track.title}`); if (hit) { geniusUrl = hit.url; await new Promise(r => setTimeout(r, 600)); @@ -629,7 +629,7 @@ export default class MediaDbPlugin extends Plugin { } } if (!spotifyUrl && spotify.isConfigured()) { - const primaryArtist = release.artists[0] ?? band.title; + const primaryArtist = release.artists[0] ?? artist.title; console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); try { spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); @@ -650,7 +650,7 @@ export default class MediaDbPlugin extends Plugin { image: release.image, subType: 'song', genres: release.genres ?? [], - artists: release.artists.length > 0 ? release.artists : [band.title], + artists: release.artists.length > 0 ? release.artists : [artist.title], albumTitle: release.title, albumReleaseGroupId: release.id, trackNumber: track.number, @@ -668,7 +668,7 @@ export default class MediaDbPlugin extends Plugin { } } - new Notice(`Finished band import for ${band.title}.`); + new Notice(`Finished artist import for ${artist.title}.`); } catch (e) { console.warn(e); new Notice(`${e}`); @@ -961,6 +961,7 @@ export default class MediaDbPlugin extends Plugin { const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); + migrateLoadedPluginSettings(loadedSettings); // Migrate property mappings using the dedicated migration method const migratedModels = PropertyMappingModel.migrateModels( diff --git a/src/models/BandModel.ts b/src/models/ArtistModel.ts similarity index 85% rename from src/models/BandModel.ts rename to src/models/ArtistModel.ts index 5b09d1d5..9683da38 100644 --- a/src/models/BandModel.ts +++ b/src/models/ArtistModel.ts @@ -3,9 +3,9 @@ import type { ModelToData } from '../utils/Utils'; import { mediaDbTag, migrateObject } from '../utils/Utils'; import { MediaTypeModel } from './MediaTypeModel'; -export type BandData = ModelToData; +export type ArtistData = ModelToData; -export class BandModel extends MediaTypeModel { +export class ArtistModel extends MediaTypeModel { genres: string[]; country: string; image: string; @@ -20,7 +20,7 @@ export class BandModel extends MediaTypeModel { personalRating: number; }; - constructor(obj: BandData) { + constructor(obj: ArtistData) { super(); this.genres = []; @@ -47,11 +47,11 @@ export class BandModel extends MediaTypeModel { } getTags(): string[] { - return [mediaDbTag, 'music', 'band']; + return [mediaDbTag, 'music', 'artist']; } getMediaType(): MediaType { - return MediaType.Band; + return MediaType.Artist; } getSummary(): string { diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 9477412f..ad1eb4c0 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,5 +1,5 @@ import type MediaDbPlugin from '../main'; -import { BandModel } from '../models/BandModel'; +import { ArtistModel } from '../models/ArtistModel'; import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { MediaType } from '../utils/MediaType'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; @@ -48,15 +48,15 @@ export class PropertyMapper { if (propertyMapping.property === key) { let finalValue = value; if (propertyMapping.wikilink) { - const useBandFileNameForArtists = + const useArtistFileNameForArtists = propertyMapping.property === 'artists' && (internalMediaType === MediaType.Song || internalMediaType === MediaType.MusicRelease); const useMusicReleaseFileNameForAlbumTitle = propertyMapping.property === 'albumTitle' && internalMediaType === MediaType.Song; if (typeof value === 'string') { - if (useBandFileNameForArtists) { - finalValue = this.bandArtistWikilink(value); + if (useArtistFileNameForArtists) { + finalValue = this.artistTitleWikilink(value); } else if (useMusicReleaseFileNameForAlbumTitle) { finalValue = this.songAlbumTitleWikilink(value, obj); } else { @@ -67,8 +67,8 @@ export class PropertyMapper { if (typeof v !== 'string') { return v; } - if (useBandFileNameForArtists) { - return this.bandArtistWikilink(v); + if (useArtistFileNameForArtists) { + return this.artistTitleWikilink(v); } if (useMusicReleaseFileNameForAlbumTitle) { return this.songAlbumTitleWikilink(v, obj); @@ -198,12 +198,12 @@ export class PropertyMapper { } /** - * Wikilink for an artist name using the Band file name template as the link target and the raw artist title as the display alias. + * Wikilink for an artist name using the Artist file name template as the link target and the raw artist title as the display alias. */ - private bandArtistWikilink(artistTitle: string): string { + private artistTitleWikilink(artistTitle: string): string { const title = artistTitle.trim(); - const bandModel = new BandModel({ - type: 'band', + const artistModel = new ArtistModel({ + type: 'artist', title, englishTitle: title, year: 0, @@ -218,10 +218,10 @@ export class PropertyMapper { genres: [], image: '', officialWebsite: '', - subType: 'band', + subType: 'artist', userData: { personalRating: 0 }, }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(bandModel); + const linkTarget = this.plugin.mediaTypeManager.getFileName(artistModel); if (linkTarget === title) { return `[[${linkTarget}]]`; } diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index c23ddf3c..2951faee 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,5 +1,5 @@ import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; -import type { MediaType } from '../utils/MediaType'; +import { MediaType } from '../utils/MediaType'; import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; // Plain object interfaces for serialization @@ -85,7 +85,7 @@ export class PropertyMappingModel { return { res: false, err: new PropertyMappingValidationError( - `Removing dataSource is only allowed for band, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, + `Removing dataSource is only allowed for artist, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, ), }; } @@ -137,6 +137,12 @@ export class PropertyMappingModel { static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { const migratedModels: PropertyMappingModel[] = []; + for (const m of loadedModels) { + if ((m.type as string) === 'band') { + (m as { type: MediaType }).type = MediaType.Artist; + } + } + for (const defaultModel of defaultModels) { const loadedModel = loadedModels.find(m => m.type === defaultModel.type); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index e74d4234..c15ad20a 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -16,7 +16,7 @@ import { FolderSuggest } from './suggesters/FolderSuggest'; function mediaTypeTabIcon(mediaType: MediaType): IconName { switch (mediaType) { - case MediaType.Band: + case MediaType.Artist: return 'mic-2'; case MediaType.BoardGame: return 'dice-3'; @@ -63,7 +63,7 @@ export interface MediaDbPluginSettings { MALAPIManga_disabledMediaTypes: MediaType[]; MobyGamesAPI_disabledMediaTypes: MediaType[]; MusicBrainzAPI_disabledMediaTypes: MediaType[]; - MusicBrainzBandAPI_disabledMediaTypes: MediaType[]; + MusicBrainzArtistAPI_disabledMediaTypes: MediaType[]; OMDbAPI_disabledMediaTypes: MediaType[]; OpenLibraryAPI_disabledMediaTypes: MediaType[]; SteamAPI_disabledMediaTypes: MediaType[]; @@ -80,7 +80,7 @@ export interface MediaDbPluginSettings { gameTemplate: string; wikiTemplate: string; musicReleaseTemplate: string; - bandTemplate: string; + artistTemplate: string; songTemplate: string; boardgameTemplate: string; bookTemplate: string; @@ -92,7 +92,7 @@ export interface MediaDbPluginSettings { gameFileNameTemplate: string; wikiFileNameTemplate: string; musicReleaseFileNameTemplate: string; - bandFileNameTemplate: string; + artistFileNameTemplate: string; songFileNameTemplate: string; boardgameFileNameTemplate: string; bookFileNameTemplate: string; @@ -104,7 +104,7 @@ export interface MediaDbPluginSettings { gameFolder: string; wikiFolder: string; musicReleaseFolder: string; - bandFolder: string; + artistFolder: string; songFolder: string; /** Frontmatter `type` for each media kind (empty = default internal id, e.g. movie, musicRelease). */ @@ -115,12 +115,12 @@ export interface MediaDbPluginSettings { gameNoteType: string; wikiNoteType: string; musicReleaseNoteType: string; - bandNoteType: string; + artistNoteType: string; songNoteType: string; boardgameNoteType: string; bookNoteType: string; - /** When true, band discography import nests albums and songs under bandFolder/BandName/… instead of using album/song import folders. */ - bandUseFileTreeForSongs: boolean; + /** When true, artist discography import nests albums and songs under artistFolder/ArtistName/… instead of using album/song import folders. */ + artistUseFileTreeForSongs: boolean; boardgameFolder: string; bookFolder: string; @@ -140,8 +140,8 @@ class MediaTypeMappedSettings { getTemplate(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Band: - return settings.bandTemplate; + case MediaType.Artist: + return settings.artistTemplate; case MediaType.BoardGame: return settings.boardgameTemplate; case MediaType.Book: @@ -167,8 +167,8 @@ class MediaTypeMappedSettings { setTemplate(settings: MediaDbPluginSettings, template: string): void { switch (this.mediaType) { - case MediaType.Band: - settings.bandTemplate = template; + case MediaType.Artist: + settings.artistTemplate = template; break; case MediaType.BoardGame: settings.boardgameTemplate = template; @@ -205,8 +205,8 @@ class MediaTypeMappedSettings { getFileNameTemplate(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Band: - return settings.bandFileNameTemplate; + case MediaType.Artist: + return settings.artistFileNameTemplate; case MediaType.BoardGame: return settings.boardgameFileNameTemplate; case MediaType.Book: @@ -232,8 +232,8 @@ class MediaTypeMappedSettings { setFileNameTemplate(settings: MediaDbPluginSettings, template: string): void { switch (this.mediaType) { - case MediaType.Band: - settings.bandFileNameTemplate = template; + case MediaType.Artist: + settings.artistFileNameTemplate = template; break; case MediaType.BoardGame: settings.boardgameFileNameTemplate = template; @@ -270,8 +270,8 @@ class MediaTypeMappedSettings { getFolder(settings: MediaDbPluginSettings): string { switch (this.mediaType) { - case MediaType.Band: - return settings.bandFolder; + case MediaType.Artist: + return settings.artistFolder; case MediaType.BoardGame: return settings.boardgameFolder; case MediaType.Book: @@ -297,8 +297,8 @@ class MediaTypeMappedSettings { setFolder(settings: MediaDbPluginSettings, folder: string): void { switch (this.mediaType) { - case MediaType.Band: - settings.bandFolder = folder; + case MediaType.Artist: + settings.artistFolder = folder; break; case MediaType.BoardGame: settings.boardgameFolder = folder; @@ -369,7 +369,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { MALAPIManga_disabledMediaTypes: [], MobyGamesAPI_disabledMediaTypes: [], MusicBrainzAPI_disabledMediaTypes: [], - MusicBrainzBandAPI_disabledMediaTypes: [], + MusicBrainzArtistAPI_disabledMediaTypes: [], OMDbAPI_disabledMediaTypes: [], OpenLibraryAPI_disabledMediaTypes: [], SteamAPI_disabledMediaTypes: [], @@ -386,7 +386,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameTemplate: '', wikiTemplate: '', musicReleaseTemplate: '', - bandTemplate: '', + artistTemplate: '', songTemplate: '', boardgameTemplate: '', bookTemplate: '', @@ -398,7 +398,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameFileNameTemplate: '{{ title }} ({{ year }})', wikiFileNameTemplate: '{{ title }}', musicReleaseFileNameTemplate: '{{ title }} ({{ FIRST:artists }} - {{ year }})', - bandFileNameTemplate: '{{ title }}', + artistFileNameTemplate: '{{ title }}', songFileNameTemplate: '{{ trackNumber }}. {{ title }} ({{ albumTitle }})', boardgameFileNameTemplate: '{{ title }} ({{ year }})', bookFileNameTemplate: '{{ title }} ({{ year }})', @@ -410,9 +410,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameFolder: 'Media DB/games', wikiFolder: 'Media DB/wiki', musicReleaseFolder: 'Media DB/music', - bandFolder: 'Media DB/bands', + artistFolder: 'Media DB/artists', songFolder: 'Media DB/music/songs', - bandUseFileTreeForSongs: false, + artistUseFileTreeForSongs: false, boardgameFolder: 'Media DB/boardgames', bookFolder: 'Media DB/books', @@ -423,7 +423,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { gameNoteType: '', wikiNoteType: '', musicReleaseNoteType: '', - bandNoteType: '', + artistNoteType: '', songNoteType: '', boardgameNoteType: '', bookNoteType: '', @@ -520,7 +520,7 @@ export class MediaDbSettingTab extends PluginSettingTab { ); } - private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Band, MediaType.MusicRelease, MediaType.Song]; + private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Artist, MediaType.MusicRelease, MediaType.Song]; private static readonly BOOK_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Book, MediaType.ComicManga]; @@ -672,23 +672,23 @@ export class MediaDbSettingTab extends PluginSettingTab { private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; - const fileTree = this.plugin.settings.bandUseFileTreeForSongs; + const fileTree = this.plugin.settings.artistUseFileTreeForSongs; panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Band), mediaTypeApiMap, { - sectionHeading: 'Band', + this.renderMediaTypeSection(panel, byType(MediaType.Artist), mediaTypeApiMap, { + sectionHeading: 'Artist', appendToSection: group => { group.addSetting( setting => void setting .setName('Use file trees for songs') .setDesc( - 'Use a file tree hierarchy to store albums and songs for each band.', + 'Use a file tree hierarchy to store albums and songs for each artist.', ) .addToggle(cb => { - cb.setValue(this.plugin.settings.bandUseFileTreeForSongs).onChange(data => { - this.plugin.settings.bandUseFileTreeForSongs = data; + cb.setValue(this.plugin.settings.artistUseFileTreeForSongs).onChange(data => { + this.plugin.settings.artistUseFileTreeForSongs = data; void this.plugin.saveSettings(); this.display(); }); @@ -951,7 +951,7 @@ export class MediaDbSettingTab extends PluginSettingTab { this.addApiSecretSetting( apiKeyGroup, 'Genius API access token', - 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing a band.', + 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing an artist.', ApiSecretID.genius, ); this.addApiSecretSetting( @@ -963,7 +963,7 @@ export class MediaDbSettingTab extends PluginSettingTab { this.addApiSecretSetting( apiKeyGroup, 'Spotify Client Secret', - 'Pair with Spotify Client ID for client-credentials access to search tracks during band import.', + 'Pair with Spotify Client ID for client-credentials access to search tracks during artist import.', ApiSecretID.spotifyClientSecret, ); }); diff --git a/src/utils/MediaType.ts b/src/utils/MediaType.ts index 3d1ad335..a9dfb548 100644 --- a/src/utils/MediaType.ts +++ b/src/utils/MediaType.ts @@ -1,5 +1,5 @@ export enum MediaType { - Band = 'band', + Artist = 'artist', BoardGame = 'boardgame', Book = 'book', ComicManga = 'comicManga', diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 560700bb..3b51f8b3 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -1,6 +1,6 @@ import type { App, TFile } from 'obsidian'; import { TFolder } from 'obsidian'; -import { BandModel } from '../models/BandModel'; +import { ArtistModel } from '../models/ArtistModel'; import { BoardGameModel } from '../models/BoardGameModel'; import { BookModel } from '../models/BookModel'; import { ComicMangaModel } from '../models/ComicMangaModel'; @@ -19,7 +19,7 @@ import { replaceTags } from './Utils'; // All media types in alphabetical order export const MEDIA_TYPES: MediaType[] = [ - MediaType.Band, + MediaType.Artist, MediaType.BoardGame, MediaType.Book, MediaType.ComicManga, @@ -45,7 +45,7 @@ export class MediaTypeManager { updateTemplates(settings: MediaDbPluginSettings): void { this.mediaFileNameTemplateMap = new Map(); - this.mediaFileNameTemplateMap.set(MediaType.Band, settings.bandFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.Artist, settings.artistFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Season, settings.seasonFileNameTemplate); @@ -58,7 +58,7 @@ export class MediaTypeManager { this.mediaFileNameTemplateMap.set(MediaType.Song, settings.songFileNameTemplate); this.mediaTemplateMap = new Map(); - this.mediaTemplateMap.set(MediaType.Band, settings.bandTemplate); + this.mediaTemplateMap.set(MediaType.Artist, settings.artistTemplate); this.mediaTemplateMap.set(MediaType.Movie, settings.movieTemplate); this.mediaTemplateMap.set(MediaType.Series, settings.seriesTemplate); this.mediaTemplateMap.set(MediaType.Season, settings.seasonTemplate); @@ -73,7 +73,7 @@ export class MediaTypeManager { updateFolders(settings: MediaDbPluginSettings): void { this.mediaFolderMap = new Map(); - this.mediaFolderMap.set(MediaType.Band, settings.bandFolder); + this.mediaFolderMap.set(MediaType.Artist, settings.artistFolder); this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder); this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder); this.mediaFolderMap.set(MediaType.Season, settings.seasonFolder); @@ -168,8 +168,8 @@ export class MediaTypeManager { return new BoardGameModel(obj); } else if (mediaType === MediaType.Book) { return new BookModel(obj); - } else if (mediaType === MediaType.Band) { - return new BandModel(obj); + } else if (mediaType === MediaType.Artist) { + return new ArtistModel(obj); } else if (mediaType === MediaType.Song) { return new SongModel(obj); } diff --git a/src/utils/noteTypeSettings.ts b/src/utils/noteTypeSettings.ts index 521f94ad..1623514a 100644 --- a/src/utils/noteTypeSettings.ts +++ b/src/utils/noteTypeSettings.ts @@ -3,7 +3,7 @@ import { MEDIA_TYPES } from './MediaTypeManager'; import { MediaType } from './MediaType'; const MEDIA_TYPE_TO_NOTE_TYPE_KEY: Record = { - [MediaType.Band]: 'bandNoteType', + [MediaType.Artist]: 'artistNoteType', [MediaType.BoardGame]: 'boardgameNoteType', [MediaType.Book]: 'bookNoteType', [MediaType.ComicManga]: 'mangaNoteType', @@ -49,6 +49,9 @@ export function resolveMetadataTypeToMediaType( if (s === 'manga') { s = MediaType.ComicManga; } + if (s === 'band') { + s = MediaType.Artist; + } for (const mt of MEDIA_TYPES) { if (mt === s) { return mt; From 2ac67e254047997f54cca5812d76f7234c9b072a Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:43:11 +0300 Subject: [PATCH 27/60] various fixes 2 --- package-lock.json | 14 ++++++++++++++ package.json | 15 ++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f1d5a3f..6d29e3c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@lemons_dev/parsinom": "^0.0.12", "@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", @@ -2126,6 +2127,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bun-types": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", diff --git a/package.json b/package.json index a6e03523..db5191d6 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,18 @@ "main": "main.js", "scripts": { "dev": "vite build --watch --mode development", - "build": "bun run tsc && vite build --mode production", + "build": "npm 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", @@ -24,6 +24,7 @@ "devDependencies": { "@lemons_dev/parsinom": "^0.0.12", "@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", From 286cfb09109a0d789bbf7fd9a7b3ec1af9474ba0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:44:05 +0300 Subject: [PATCH 28/60] varios fixes 1 --- src/api/APIManager.ts | 2 +- src/api/APIModel.ts | 15 ++ src/api/apis/IGDBAPI.ts | 21 +- src/api/apis/MusicBrainzAPI.ts | 23 ++ src/api/apis/TMDBMovieAPI.ts | 11 +- src/api/apis/TMDBSeriesAPI.ts | 14 +- src/main.ts | 342 ++++++++++++++++++++++++--- src/modals/BulkUpdateConfirmModal.ts | 39 +++ src/modals/CompletionModal.ts | 85 +++++++ src/modals/ConfirmOverwriteModal.ts | 1 + src/models/GameModel.ts | 10 +- src/models/MovieModel.ts | 4 +- src/models/SeriesModel.ts | 6 +- src/settings/PropertyMapper.ts | 131 +++------- src/settings/PropertyMapping.ts | 8 +- src/settings/Settings.ts | 174 ++++++++++++-- src/styles.css | 75 ++++++ src/utils/AutoTrackerHelper.ts | 79 +++++++ src/utils/BulkImportHelper.ts | 21 +- src/utils/BulkUpdateHelper.ts | 53 +++++ src/utils/Utils.ts | 5 +- src/utils/noteTypeSettings.ts | 3 - 22 files changed, 946 insertions(+), 176 deletions(-) create mode 100644 src/modals/BulkUpdateConfirmModal.ts create mode 100644 src/modals/CompletionModal.ts create mode 100644 src/utils/AutoTrackerHelper.ts create mode 100644 src/utils/BulkUpdateHelper.ts diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index a244ce26..bcdd5401 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -51,7 +51,7 @@ export class APIManager { /** * Queries detailed info for an id from an API. - * MusicBrainz-backed notes use on-disk dataSource `MusicBrainz`; `mediaType` picks artist vs release/song API. + * MusicBrainz-backed notes use on-disk dataSource `MusicBrainz`; `mediaType` picks Artist vs release/song API. * * @param id * @param apiName Stored dataSource on the note, or an exact {@link APIModel.apiName} (e.g. bulk import / ID search). diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index 0919db90..50e6d72c 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -28,4 +28,19 @@ export abstract class APIModel { hasTypeOverlap(types: MediaType[]): boolean { return types.some(type => this.hasType(type)); } + + /** + * Returns the wiki-link string for a given property value. + * Subclasses can override this to apply API-specific file name templates + * (e.g. using an Artist file name for artist links). + * + * @param _property the property key (e.g. 'artists', 'albumTitle') + * @param value the raw string value to wrap + * @param _obj the full metadata object (for context) + * @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/') + */ + wikilinkValueFor(_property: string, value: string, _obj: Record, folderPrefix: string): string { + const clean = value.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; + return `[[${folderPrefix}${clean}|${clean}]]`; + } } diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 2c55315a..bd88719a 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -11,10 +11,15 @@ interface IGDBCover { url: string; } interface IGDBGenre { name: string; } interface IGDBCompany { name: string; } interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } +interface IGDBPlatform { name: string; } +interface IGDBGameMode { name: string; } +interface IGDBCollection { name: string; } interface IGDBGame { id: number; name: string; cover?: IGDBCover; first_release_date?: number; summary?: string; total_rating?: number; url?: string; genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; + platforms?: IGDBPlatform[]; game_modes?: IGDBGameMode[]; + collection?: IGDBCollection; collections?: IGDBCollection[]; franchises?: IGDBCollection[]; } interface TwitchAuthResponse { access_token: string; expires_in: number; } @@ -69,7 +74,7 @@ export class IGDBAPI extends APIModel { const data = response.json as IGDBGame[]; return data.map(result => { const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; return new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name, year: coerceYear(year), dataSource: this.apiName, id: result.id.toString(), image: image @@ -81,7 +86,7 @@ export class IGDBAPI extends APIModel { console.log(`MDB | api "${this.apiName}" queried by ID`); const token = await this.getAuthToken(); const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); - const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; where id = ${id};`; + const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher, platforms.name, game_modes.name, collection.name, collections.name, franchises.name; where id = ${id};`; const response = await requestUrl({ url: `${this.apiUrl}/games`, method: 'POST', headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, @@ -100,7 +105,12 @@ export class IGDBAPI extends APIModel { if (c.publisher) publishers.push(c.company.name); }); const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_cover_big') : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; + + let combinedSeries: string[] = []; + if (result.collection?.name) combinedSeries.push(result.collection.name); + result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); + result.franchises?.forEach(f => { if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); }); return new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name, @@ -108,8 +118,11 @@ export class IGDBAPI extends APIModel { result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0, ), dataSource: this.apiName, url: result.url, id: result.id.toString(), + summary: result.summary ?? '', series: combinedSeries, + gameModes: result.game_modes?.map(g => g.name) || [], platforms: result.platforms?.map(p => p.name) || [], developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], - onlineRating: result.total_rating, image: image, released: true, + onlineRating: result.total_rating ? Math.round(result.total_rating * 10) / 10 : 0, image: image, + released: result.first_release_date ? (result.first_release_date * 1000) <= Date.now() : false, releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', userData: { played: false, personalRating: 0 }, }); diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index 76bb4b67..df6d0d16 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -235,6 +235,29 @@ export class MusicBrainzAPI extends APIModel { }, }); } + /** + * For the 'albumTitle' property (used on Song notes), the link target is + * derived from the MusicRelease file name template rather than the raw title. + */ + override wikilinkValueFor(property: string, value: string, obj: Record, folderPrefix: string): string { + if (property === 'albumTitle') { + const title = value.trim(); + const artistsRaw = obj.artists; + const artists = Array.isArray(artistsRaw) + ? artistsRaw.filter((a): a is string => typeof a === 'string') + : []; + const releaseModel = new MusicReleaseModel({ + type: 'musicRelease', title, englishTitle: title, + year: coerceYear(obj.year), releaseDate: '', dataSource: '', + url: '', id: '', image: '', artists, genres: [], + subType: 'album', language: '', rating: 0, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); + return linkTarget === title ? `[[${linkTarget}]]` : `[[${linkTarget}|${title}]]`; + } + return super.wikilinkValueFor(property, value, obj, folderPrefix); + } + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; } diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index 0f2e40dd..ea0cd19a 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -134,7 +134,7 @@ export class TMDBMovieAPI extends APIModel { params: { path: { movie_id: parseInt(id) }, query: { - append_to_response: 'credits', + append_to_response: 'credits,release_dates,watch/providers', }, }, fetch: fetch, @@ -173,15 +173,20 @@ export class TMDBMovieAPI extends APIModel { studio: result.production_companies?.map((s: any) => s.name) ?? [], duration: result.runtime != null && Number.isFinite(result.runtime) ? Math.trunc(result.runtime) : 0, - onlineRating: result.vote_average, + onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, // @ts-ignore 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!), + country: result.production_countries?.map((c: any) => c.name) ?? [], + language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], budget: formatUsdWholeDollars(result.budget ?? 0), revenue: formatUsdWholeDollars(result.revenue ?? 0), - streamingServices: [], + // @ts-ignore + ageRating: result.release_dates?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.release_dates?.[0]?.certification ?? '', + // @ts-ignore + streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], userData: { watched: false, diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 8ffd5919..61fe68f3 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -101,7 +101,7 @@ export class TMDBSeriesAPI extends APIModel { params: { path: { series_id: parseInt(id) }, query: { - append_to_response: 'credits', + append_to_response: 'credits,content_ratings,watch/providers', }, }, fetch: fetch, @@ -136,14 +136,20 @@ export class TMDBSeriesAPI extends APIModel { studio: result.production_companies?.map((s: any) => s.name) ?? [], episodes: result.number_of_episodes, duration: result.episode_run_time?.[0]?.toString() ?? 'unknown', - onlineRating: result.vote_average, + onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type // @ts-ignore actors: result.credits?.cast.map((c: any) => c.name).slice(0, 5) ?? [], image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null, - released: ['Returning Series', 'Cancelled', 'Ended'].includes(result.status!), - streamingServices: [], + released: ['Returning Series', 'Cancelled', 'Canceled', 'Pilot', 'Ended'].includes(result.status!), + country: result.production_countries?.map((c: any) => c.name) ?? [], + language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], + network: result.networks?.map((n: any) => n.name) ?? [], + // @ts-ignore + ageRating: result.content_ratings?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.rating ?? '', + // @ts-ignore + streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], 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'), diff --git a/src/main.ts b/src/main.ts index 81a752e4..9e655a91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,4 @@ -import type { TFile } from 'obsidian'; -import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder } from 'obsidian'; +import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder, TFile } from 'obsidian'; import { requestUrl, normalizePath } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; @@ -25,6 +24,8 @@ import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { GeniusClient } from './api/GeniusClient'; import { SpotifyClient } from './api/SpotifyClient'; import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; +import { BulkUpdateConfirmModal } from './modals/BulkUpdateConfirmModal'; +import { CompletionModal } from './modals/CompletionModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import type { ArtistModel } from './models/ArtistModel'; @@ -36,8 +37,10 @@ import { ApiSecretID, getApiSecretValue } from './settings/apiSecretsHelper'; import { PropertyMapper } from './settings/PropertyMapper'; import { PropertyMappingModel } from './settings/PropertyMapping'; import type { MediaDbPluginSettings } from './settings/Settings'; -import { getDefaultSettings, MediaDbSettingTab, migrateLoadedPluginSettings, propertyMappingModelsInDisplayOrder } from './settings/Settings'; +import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOrder } from './settings/Settings'; +import { AutoTrackerHelper } from './utils/AutoTrackerHelper'; import { BulkImportHelper } from './utils/BulkImportHelper'; +import { BulkUpdateHelper } from './utils/BulkUpdateHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; @@ -63,6 +66,8 @@ export default class MediaDbPlugin extends Plugin { modelPropertyMapper!: PropertyMapper; modalHelper!: ModalHelper; bulkImportHelper!: BulkImportHelper; + bulkUpdateHelper!: BulkUpdateHelper; + autoTrackerHelper!: AutoTrackerHelper; dateFormatter!: DateFormatter; frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; @@ -93,6 +98,8 @@ export default class MediaDbPlugin extends Plugin { this.modelPropertyMapper = new PropertyMapper(this); this.modalHelper = new ModalHelper(this); this.bulkImportHelper = new BulkImportHelper(this); + this.bulkUpdateHelper = new BulkUpdateHelper(this); + this.autoTrackerHelper = new AutoTrackerHelper(this); this.dateFormatter = new DateFormatter(); await this.loadSettings(); @@ -103,22 +110,97 @@ export default class MediaDbPlugin extends Plugin { this.mediaTypeManager.updateFolders(this.settings); this.dateFormatter.setFormat(this.settings.customDateFormat); - // add icon to the left ribbon - const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.createEntryWithAdvancedSearchModal()); - ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); + // add icon to the left ribbon and auto-tracker logic + this.refreshAutoTrackerRibbon(); + + this.app.workspace.onLayoutReady(() => { + if (this.settings.autoUpdateAiringMode) { + setTimeout(() => { + this.autoTrackerHelper.startBackgroundScan(true); + }, 5000); + } + }); this.registerEvent( this.app.workspace.on('file-menu', (menu, file) => { if (file instanceof TFolder) { + // Add our customized context menu options under a "Media DB" group menu.addItem(item => { - item.setTitle('Import folder as Media DB entries') - .setIcon('database') - .onClick(() => this.bulkImportHelper.import(file)); + item.setTitle('Media DB...'); + item.setIcon('database'); + // @ts-ignore + if (typeof item.setSubmenu === 'function') { + // @ts-ignore + const sub = item.setSubmenu(); + sub.addItem((subItem: any) => subItem.setTitle('Bulk Import Folder').setIcon('database').onClick(() => this.bulkImportHelper.import(file))); + sub.addItem((subItem: any) => subItem.setTitle('Bulk Update Metadata').setIcon('refresh-cw').onClick(() => this.bulkUpdateHelper.updateFolder(file))); + sub.addItem((subItem: any) => subItem.setTitle('Start Auto-Tracker in Folder').setIcon('sync').onClick(() => { + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); + }).open(); + })); + sub.addItem((subItem: any) => subItem.setTitle('Download images in folder').setIcon('image').onClick(() => this.downloadImagesInFolder(file))); + } else { + // Fallback if setSubmenu isn't in older Obsidian versions + item.onClick(() => this.bulkUpdateHelper.updateFolder(file)); + } }); } }), ); + this.addCommand({ + id: 'media-db-bulk-import-active-file-folder', + name: 'Media DB: Bulk Import Folder (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.bulkImportHelper.import(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-bulk-update-active-file-folder', + name: 'Media DB: Bulk Update Metadata (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.bulkUpdateHelper.updateFolder(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-download-images-active-file-folder', + name: 'Media DB: Download images in folder (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.downloadImagesInFolder(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-download-images-active-note', + name: 'Media DB: Download images in active note', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || activeFile.extension !== 'md') return false; + if (!checking) void this.downloadImagesInFile(activeFile); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-manual-sync-auto-tracker', + name: 'Media DB: Force Auto-Tracker Background Scan', + callback: () => this.autoTrackerHelper.startBackgroundScan(false), + }); + // register command to open search modal this.addCommand({ id: 'open-media-db-search-modal', @@ -558,7 +640,7 @@ export default class MediaDbPlugin extends Plugin { }; const artistBaseFolder = await this.mediaTypeManager.getFolder(artist, this.app); - let artistNoteFolder = artistBaseFolder; + const artistNoteFolder = artistBaseFolder; let albumNotesFolder = artistBaseFolder; if (useTree) { @@ -709,6 +791,67 @@ export default class MediaDbPlugin extends Plugin { return false; } + async downloadImagesInFolder(folder: TFolder): Promise { + new Notice(`MDB | Scanning for images to download in ${folder.name}...`); + const files = folder.children.filter((c): c is TFile => c instanceof TFile && c.extension === 'md'); + const startTime = Date.now(); + let downloaded = 0; + let failed = 0; + for (const file of files) { + const result = await this.downloadImagesInFile(file, true); + if (result === true) downloaded++; + else if (result === false) failed++; + // null means no image to download + if (result !== null) { + await new Promise(r => setTimeout(r, 600)); // anti-rate limit + } + } + new CompletionModal(this.app, { + title: 'Image Download Complete', + icon: '🖼️', + total: downloaded + failed, + success: downloaded, + errors: failed, + elapsedMs: Date.now() - startTime, + notes: failed > 0 ? ['Some images could not be downloaded. Check the console for details.'] : [], + }).open(); + } + + /** + * Downloads images for a single file. + * @returns true if downloaded, false if failed, null if nothing to download + */ + async downloadImagesInFile(file: TFile, silent: boolean = false): Promise { + const metadata = this.getMetadataFromFileCache(file); + if (typeof metadata.image === 'string' && metadata.image.startsWith('http')) { + try { + const imageUrl = metadata.image; + const extMatch = imageUrl.split('?')[0].match(/\.([a-zA-Z0-9]+)$/); + const ext = extMatch ? extMatch[1] : 'jpg'; + const imgName = replaceIllegalFileNameCharactersInString(file.basename) + '.' + ext; + const imgFolder = await this.ensureVaultFolder(this.settings.imageFolder); + const imagePath = `${imgFolder.path}/${imgName}`; + + if (!this.app.vault.getAbstractFileByPath(imagePath)) { + const response = await requestUrl({ url: imageUrl, method: 'GET' }); + await this.app.vault.createBinary(imagePath, response.arrayBuffer); + } + + await this.app.fileManager.processFrontMatter(file, (frontmatter: any) => { + frontmatter.image = `[[${imagePath}]]`; + }); + if (!silent) new Notice(`MDB | Image downloaded for ${file.basename}`); + return true; + } catch (e) { + console.error("MDB | Image download failed for", file.path, e); + if (!silent) new Notice(`MDB | Image download failed for ${file.basename}`); + return false; + } + } + if (!silent) new Notice(`MDB | No external image found in ${file.basename}`); + return null; + } + private metadataRecordForNewNote(mediaTypeModel: MediaTypeModel): Record { let meta: Record; if (this.settings.useDefaultFrontMatter) { @@ -720,28 +863,7 @@ export default class MediaDbPlugin extends Plugin { dataSource: mediaTypeModel.dataSource, }; } - return this.withNormalizedTitleAliasMetadata(meta, mediaTypeModel.title); - } - - private withNormalizedTitleAliasMetadata(meta: Record, title: string): Record { - if (!this.settings.addNormalizeTitlesAsAlias) { - return meta; - } - const alias = normalizeTitleForAsciiAlias(title); - if (alias === null) { - return meta; - } - const prev = meta['aliases']; - let list: string[] = []; - if (Array.isArray(prev)) { - list = prev.filter((x): x is string => typeof x === 'string'); - } else if (typeof prev === 'string' && prev.length > 0) { - list = [prev]; - } - if (!list.includes(alias)) { - list = [...list, alias]; - } - return { ...meta, aliases: list }; + return meta; } generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { @@ -770,6 +892,62 @@ export default class MediaDbPlugin extends Plugin { ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); + // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- + const entityWikiProps = this.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + if (entityWikiProps.length > 0) { + const folderPrefix = this.settings.wikiFolder ? `${this.settings.wikiFolder}/` : ''; + const isEnabled = this.settings.enableWikiLinkParsing; + const formatWiki = (v: unknown) => { + if (typeof v !== 'string') return v; + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + return isEnabled ? `[[${folderPrefix}${clean}|${clean}]]` : clean.trim(); + }; + + for (const [key, value] of Object.entries(fileMetadata)) { + if (key === 'aliases') continue; + if (entityWikiProps.includes(key.toLowerCase())) { + if (typeof value === 'string') { + fileMetadata[key] = formatWiki(value); + } else if (Array.isArray(value)) { + fileMetadata[key] = value.map(formatWiki); + } + } + } + } + + // --- Auto-Tag Logic --- + const tagProps = this.settings.autoTagProperties.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + if (this.settings.enableAutoTagging && tagProps.length > 0) { + const existingTags: string[] = Array.isArray(fileMetadata['tags']) ? (fileMetadata['tags'] as string[]) : []; + const newTags = new Set(existingTags.filter(t => typeof t === 'string' && t.trim() !== '')); + + for (const [key, value] of Object.entries(fileMetadata)) { + if (tagProps.includes(key.toLowerCase()) && value) { + const valuesToTag = Array.isArray(value) ? value : [value]; + for (let v of valuesToTag) { + if (typeof v === 'string') { + v = String(v).replace(/^\[\[(.*?)\]\]$/, '$1'); + if (v.includes('|')) { + v = v.split('|')[1]; + } + const sanitized = v + .trim() + .replace(/\s+/g, '-') + .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') + .toLowerCase(); + + if (sanitized) newTags.add(sanitized); + } + } + } + } + + if (newTags.size > 0) { + fileMetadata['tags'] = Array.from(newTags); + } + } + if (mediaTypeModel.getMediaType() === MediaType.Song) { const song = mediaTypeModel as SongModel; if(song.lyrics.length > 0) { @@ -788,15 +966,61 @@ export default class MediaDbPlugin extends Plugin { return fileContent; } + extractManualTags(metadata: Record, autoTagKeys: string): string[] { + const allTagsRaw = metadata['tags']; + const allTags = Array.isArray(allTagsRaw) ? allTagsRaw : typeof allTagsRaw === 'string' ? [allTagsRaw] : []; + if (allTags.length === 0) return []; + + const tagProps = autoTagKeys.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + const autoTagValues = new Set(); + + for (const [key, value] of Object.entries(metadata)) { + if (tagProps.includes(key.toLowerCase()) && value) { + const valuesToTag = Array.isArray(value) ? value : [value]; + for (const v of valuesToTag) { + if (typeof v === 'string') { + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + const sanitized = clean.trim().replace(/\s+/g, '-').replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '').toLowerCase(); + if (sanitized) autoTagValues.add(sanitized); + } + } + } + } + + return allTags.map(t => String(t).trim()).filter(t => t && !autoTagValues.has(t.toLowerCase()) && !t.toLowerCase().startsWith('mediadb/')); + } + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!fileToAttach) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); + + // Rescue arrays that Object.assign would normally crush + const rescueArray = (key: string) => { + const arr = attachFileMetadata[key]; + if (Array.isArray(arr)) return [...arr as string[]]; + if (typeof arr === 'string' && arr.trim()) return [arr]; + return []; + }; + const oldManualTags = this.extractManualTags(attachFileMetadata, this.settings.autoTagProperties); + const oldAliases = rescueArray('aliases'); + // TODO: better object merging fileMetadata = Object.assign(attachFileMetadata, fileMetadata); + // Merge tags cleanly (Preserving only manual user tags, discarding old ghost auto-tags!) + const newObjTags = fileMetadata['tags']; + const finalTags = new Set([...oldManualTags, ...(Array.isArray(newObjTags) ? newObjTags : typeof newObjTags === 'string' ? [newObjTags] : [])].map(t => String(t).trim())); + if (finalTags.size > 0) fileMetadata['tags'] = Array.from(finalTags); + + // Merge aliases cleanly + const newObjAliases = fileMetadata['aliases']; + const finalAliases = new Set([...oldAliases, ...(Array.isArray(newObjAliases) ? newObjAliases : typeof newObjAliases === 'string' ? [newObjAliases] : [])].map(a => String(a).trim())); + if (finalAliases.size > 0) fileMetadata['aliases'] = Array.from(finalAliases); + let attachFileContent: string = await this.app.vault.read(fileToAttach); const regExp = new RegExp(this.frontMatterRexExpPattern); attachFileContent = attachFileContent.replace(regExp, ''); @@ -874,9 +1098,12 @@ export default class MediaDbPlugin extends Plugin { // look if file already exists and ask if it should be overwritten const file = this.app.vault.getAbstractFileByPath(filePath); if (file) { - const shouldOverwrite = await new Promise(resolve => { - new ConfirmOverwriteModal(this.app, fileName, resolve).open(); - }); + let shouldOverwrite = options.overwrite; + if (!shouldOverwrite) { + shouldOverwrite = await new Promise(resolve => { + new ConfirmOverwriteModal(this.app, fileName, resolve).open(); + }); + } if (!shouldOverwrite) { throw new Error('MDB | file creation cancelled by user'); @@ -902,6 +1129,31 @@ export default class MediaDbPlugin extends Plugin { return targetFile; } + // --- AutoTracker Ribbon Logic --- + public _ribbonEl: HTMLElement | null = null; + refreshAutoTrackerRibbon() { + if (!this._ribbonEl) { + this._ribbonEl = this.addRibbonIcon('sync', 'Media DB: Auto-Tracker Sync', () => { + if (this.autoTrackerHelper.isScanning) { + new Notice("Auto-Tracker is currently syncing in the background."); + } else { + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate); + }).open(); + } + }); + this._ribbonEl.addClass('obsidian-media-db-plugin-ribbon-class'); + } + + if (this.autoTrackerHelper.isScanning) { + this._ribbonEl.addClass('media-db-spin-animation'); + } else { + this._ribbonEl.removeClass('media-db-spin-animation'); + } + } + /** * Update the active note by querying the API again. * Tries to read the type and id of the active note (and dataSource when required). If successful it will query the api, delete the old note and create a new one. @@ -911,6 +1163,10 @@ export default class MediaDbPlugin extends Plugin { if (!activeFile) { throw new Error('MDB | there is no active note'); } + return this.updateNote(activeFile, onlyMetadata, true, false); + } + + async updateNote(activeFile: TFile, onlyMetadata: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { let metadata = this.getMetadataFromFileCache(activeFile); metadata = this.modelPropertyMapper.convertObjectBack(metadata); @@ -951,9 +1207,9 @@ export default class MediaDbPlugin extends Plugin { console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: true }); + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); } else { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: true }); + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); } } @@ -961,7 +1217,6 @@ export default class MediaDbPlugin extends Plugin { const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); - migrateLoadedPluginSettings(loadedSettings); // Migrate property mappings using the dedicated migration method const migratedModels = PropertyMappingModel.migrateModels( @@ -972,6 +1227,17 @@ export default class MediaDbPlugin extends Plugin { // Store as plain data for serialization (canonical order matches settings UI) loadedSettings.propertyMappingModels = propertyMappingModelsInDisplayOrder(migratedModels.map(m => m.toJSON())); + // --- MIGRATION: Band to Artist --- + const anyLoaded = diskSettings as any; + if (anyLoaded) { + if (anyLoaded.bandTemplate && !loadedSettings.artistTemplate) loadedSettings.artistTemplate = anyLoaded.bandTemplate; + if (anyLoaded.bandFolder && !loadedSettings.artistFolder) loadedSettings.artistFolder = anyLoaded.bandFolder; + if (anyLoaded.bandFileNameTemplate && !loadedSettings.artistFileNameTemplate) loadedSettings.artistFileNameTemplate = anyLoaded.bandFileNameTemplate; + if (anyLoaded.bandNoteType && !loadedSettings.artistNoteType) loadedSettings.artistNoteType = anyLoaded.bandNoteType; + if (anyLoaded.bandUseFileTreeForSongs !== undefined && loadedSettings.artistUseFileTreeForSongs === false) loadedSettings.artistUseFileTreeForSongs = anyLoaded.bandUseFileTreeForSongs; + if (anyLoaded.MusicBrainzBandAPI_disabledMediaTypes && !loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes) loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes = anyLoaded.MusicBrainzBandAPI_disabledMediaTypes; + } + this.settings = loadedSettings; } diff --git a/src/modals/BulkUpdateConfirmModal.ts b/src/modals/BulkUpdateConfirmModal.ts new file mode 100644 index 00000000..41c34555 --- /dev/null +++ b/src/modals/BulkUpdateConfirmModal.ts @@ -0,0 +1,39 @@ +import { App, Modal, Setting } from 'obsidian'; + +export class BulkUpdateConfirmModal extends Modal { + onSubmit: (silent: boolean) => void; + silentUpdate: boolean = true; + + constructor(app: App, onSubmit: (silent: boolean) => void) { + super(app); + this.onSubmit = onSubmit; + } + + onOpen() { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'Bulk Update Metadata' }); + contentEl.createEl('p', { text: 'You are about to scan and update metadata for notes in this folder.' }); + + new Setting(contentEl) + .setName('Update Silently (No Confirmations)') + .setDesc('If enabled, all updates will aggressively overwrite the note frontmatter without asking for individual confirmation for each file.') + .addToggle(toggle => toggle + .setValue(this.silentUpdate) + .onChange(value => (this.silentUpdate = value)) + ); + + new Setting(contentEl) + .addButton(btn => btn + .setButtonText('Start Update') + .setCta() + .onClick(() => { + this.close(); + this.onSubmit(this.silentUpdate); + })); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/modals/CompletionModal.ts b/src/modals/CompletionModal.ts new file mode 100644 index 00000000..ac373dd6 --- /dev/null +++ b/src/modals/CompletionModal.ts @@ -0,0 +1,85 @@ +import { App, Modal, ButtonComponent } from 'obsidian'; + +export interface CompletionResult { + /** Title shown in the modal header */ + title: string; + /** Icon emoji for the operation type */ + icon?: string; + /** Total number of items processed */ + total: number; + /** Number of successfully processed items */ + success: number; + /** Number of failed items */ + errors: number; + /** Number of skipped items (optional) */ + skipped?: number; + /** Elapsed time in milliseconds */ + elapsedMs?: number; + /** Optional extra lines shown below the stats */ + notes?: string[]; +} + +export class CompletionModal extends Modal { + private result: CompletionResult; + + constructor(app: App, result: CompletionResult) { + super(app); + this.result = result; + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('mdb-completion-modal'); + + const r = this.result; + const icon = r.icon ?? '✅'; + const allSuccess = r.errors === 0; + + // Header + const header = contentEl.createEl('div', { cls: 'mdb-completion-header' }); + header.createEl('span', { cls: 'mdb-completion-icon', text: allSuccess ? icon : '⚠️' }); + header.createEl('h2', { cls: 'mdb-completion-title', text: r.title }); + + // Stats + const stats = contentEl.createEl('div', { cls: 'mdb-completion-stats' }); + + this.addStatRow(stats, '📄 Total', `${r.total}`); + this.addStatRow(stats, '✅ Successful', `${r.success}`, 'success'); + this.addStatRow(stats, '❌ Errors', `${r.errors}`, r.errors > 0 ? 'error' : undefined); + + if (r.skipped !== undefined) { + this.addStatRow(stats, '⏭️ Skipped', `${r.skipped}`, 'skipped'); + } + + if (r.elapsedMs !== undefined) { + const secs = (r.elapsedMs / 1000).toFixed(1); + this.addStatRow(stats, '⏱️ Duration', `${secs}s`); + } + + // Notes + if (r.notes && r.notes.length > 0) { + const notesEl = contentEl.createEl('div', { cls: 'mdb-completion-notes' }); + for (const note of r.notes) { + notesEl.createEl('p', { text: note }); + } + } + + // Close button + const footer = contentEl.createEl('div', { cls: 'mdb-completion-footer' }); + new ButtonComponent(footer) + .setButtonText('Close') + .setCta() + .onClick(() => this.close()); + } + + onClose(): void { + this.contentEl.empty(); + } + + private addStatRow(container: HTMLElement, label: string, value: string, cls?: string): void { + const row = container.createEl('div', { cls: 'mdb-completion-row' }); + row.createEl('span', { cls: 'mdb-completion-label', text: label }); + row.createEl('span', { cls: `mdb-completion-value${cls ? ' mdb-stat-' + cls : ''}`, text: value }); + } +} diff --git a/src/modals/ConfirmOverwriteModal.ts b/src/modals/ConfirmOverwriteModal.ts index e300c325..9852da26 100644 --- a/src/modals/ConfirmOverwriteModal.ts +++ b/src/modals/ConfirmOverwriteModal.ts @@ -20,6 +20,7 @@ export class ConfirmOverwriteModal extends Modal { contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); const bottomSettingRow = new Setting(contentEl); + bottomSettingRow.addButton(btn => { btn.setButtonText('No'); btn.onClick(() => this.close()); diff --git a/src/models/GameModel.ts b/src/models/GameModel.ts index 5661f6ec..7cde3425 100644 --- a/src/models/GameModel.ts +++ b/src/models/GameModel.ts @@ -11,6 +11,10 @@ export class GameModel extends MediaTypeModel { genres: string[]; onlineRating: number; image: string; + summary: string; + series: string[]; + gameModes: string[]; + platforms: string[]; released: boolean; releaseDate: string; @@ -28,6 +32,10 @@ export class GameModel extends MediaTypeModel { this.genres = []; this.onlineRating = 0; this.image = ''; + this.summary = ''; + this.series = []; + this.gameModes = []; + this.platforms = []; this.released = false; this.releaseDate = ''; @@ -55,6 +63,6 @@ export class GameModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + this.year + ')'; + return this.englishTitle + ' (' + (this.year > 0 ? this.year : '') + ')'; } } diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index a0dcaf91..ddd15c01 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -20,6 +20,7 @@ export class MovieModel extends MediaTypeModel { released: boolean; country: string[]; + language: string[]; /** Production budget in USD (e.g. from TMDB). */ budget: string; /** Box-office gross (e.g. worldwide from TMDB; OMDb US figure when from IMDb). */ @@ -50,6 +51,7 @@ export class MovieModel extends MediaTypeModel { this.released = false; this.country = []; + this.language = []; this.budget = ''; this.revenue = ''; this.ageRating = ''; @@ -81,6 +83,6 @@ export class MovieModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + this.year + ')'; + return this.englishTitle + ' (' + (this.year > 0 ? this.year : '') + ')'; } } diff --git a/src/models/SeriesModel.ts b/src/models/SeriesModel.ts index b87a32c0..2272b697 100644 --- a/src/models/SeriesModel.ts +++ b/src/models/SeriesModel.ts @@ -19,6 +19,8 @@ export class SeriesModel extends MediaTypeModel { released: boolean; country: string[]; + language: string[]; + network: string[]; ageRating: string; streamingServices: string[]; airing: boolean; @@ -47,6 +49,8 @@ export class SeriesModel extends MediaTypeModel { this.released = false; this.country = []; + this.language = []; + this.network = []; this.ageRating = ''; this.streamingServices = []; this.airing = false; @@ -77,6 +81,6 @@ export class SeriesModel extends MediaTypeModel { } getSummary(): string { - return this.title + ' (' + this.year + ')'; + return this.title + ' (' + (this.year > 0 ? this.year : '') + ')'; } } diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index ad1eb4c0..1ff0c809 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,9 +1,5 @@ import type MediaDbPlugin from '../main'; -import { ArtistModel } from '../models/ArtistModel'; -import { MusicReleaseModel } from '../models/MusicReleaseModel'; -import { MediaType } from '../utils/MediaType'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; -import { coerceYear } from '../utils/Utils'; import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { @@ -40,6 +36,31 @@ export class PropertyMapper { const newObj: Record = {}; + const entityProps = this.plugin.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + + // 1. Preprocess global wiki-links on the raw object first + if (this.plugin.settings.enableWikiLinkParsing && entityProps.length > 0) { + for (const [key, value] of Object.entries(obj)) { + if (key === 'aliases') continue; + if (entityProps.includes(key.toLowerCase())) { + const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; + const formatWiki = (v: unknown) => { + if (typeof v !== 'string') return v; + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + return `[[${folderPrefix}${clean}|${clean}]]`; + }; + + if (typeof value === 'string') { + obj[key] = formatWiki(value); + } else if (Array.isArray(value)) { + obj[key] = value.map(formatWiki); + } + } + } + } + + // 2. Map standard properties for (const [key, value] of Object.entries(obj)) { if (key === 'aliases') { continue; @@ -48,33 +69,21 @@ export class PropertyMapper { if (propertyMapping.property === key) { let finalValue = value; if (propertyMapping.wikilink) { - const useArtistFileNameForArtists = - propertyMapping.property === 'artists' && - (internalMediaType === MediaType.Song || internalMediaType === MediaType.MusicRelease); - const useMusicReleaseFileNameForAlbumTitle = - propertyMapping.property === 'albumTitle' && internalMediaType === MediaType.Song; - + const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; + // Resolve the originating API so it can provide property-specific link formatting + const api = typeof obj.dataSource === 'string' + ? this.plugin.apiManager.getApiByName(obj.dataSource) + : undefined; + const wikilink = (v: unknown): unknown => { + if (typeof v !== 'string') return v; + if (api) return api.wikilinkValueFor(key, v, obj, folderPrefix); + const clean = v.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; + return `[[${folderPrefix}${clean}|${clean}]]`; + }; if (typeof value === 'string') { - if (useArtistFileNameForArtists) { - finalValue = this.artistTitleWikilink(value); - } else if (useMusicReleaseFileNameForAlbumTitle) { - finalValue = this.songAlbumTitleWikilink(value, obj); - } else { - finalValue = `[[${value}]]`; - } + finalValue = wikilink(value); } else if (Array.isArray(value)) { - finalValue = value.map((v: unknown) => { - if (typeof v !== 'string') { - return v; - } - if (useArtistFileNameForArtists) { - return this.artistTitleWikilink(v); - } - if (useMusicReleaseFileNameForAlbumTitle) { - return this.songAlbumTitleWikilink(v, obj); - } - return `[[${v}]]`; - }); + finalValue = value.map(wikilink); } } if (propertyMapping.mapping === PropertyMappingOption.Map) { @@ -197,68 +206,4 @@ export class PropertyMapper { return originalObj; } - /** - * Wikilink for an artist name using the Artist file name template as the link target and the raw artist title as the display alias. - */ - private artistTitleWikilink(artistTitle: string): string { - const title = artistTitle.trim(); - const artistModel = new ArtistModel({ - type: 'artist', - title, - englishTitle: title, - year: 0, - beginYear: '', - releaseDate: '', - dataSource: '', - url: '', - id: '', - country: '', - disambiguation: '', - isni: '', - genres: [], - image: '', - officialWebsite: '', - subType: 'artist', - userData: { personalRating: 0 }, - }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(artistModel); - if (linkTarget === title) { - return `[[${linkTarget}]]`; - } - return `[[${linkTarget}|${title}]]`; - } - - /** - * Wikilink for a song's album title using the Music Release file name template; fills artists/year from the song metadata when present. - */ - private songAlbumTitleWikilink(albumTitle: string, songMeta: Record): string { - const title = albumTitle.trim(); - const artistsRaw = songMeta.artists; - const artists = Array.isArray(artistsRaw) - ? artistsRaw.filter((a): a is string => typeof a === 'string') - : []; - const year = coerceYear(songMeta.year); - const releaseModel = new MusicReleaseModel({ - type: 'musicRelease', - title, - englishTitle: title, - year, - releaseDate: '', - dataSource: '', - url: '', - id: '', - image: '', - artists, - genres: [], - subType: 'album', - language: '', - rating: 0, - userData: { personalRating: 0 }, - }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); - if (linkTarget === title) { - return `[[${linkTarget}]]`; - } - return `[[${linkTarget}|${title}]]`; - } } diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index 2951faee..649320e9 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,5 +1,5 @@ import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; -import { MediaType } from '../utils/MediaType'; +import type { MediaType } from '../utils/MediaType'; import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; // Plain object interfaces for serialization @@ -137,12 +137,6 @@ export class PropertyMappingModel { static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { const migratedModels: PropertyMappingModel[] = []; - for (const m of loadedModels) { - if ((m.type as string) === 'band') { - (m as { type: MediaType }).type = MediaType.Artist; - } - } - for (const defaultModel of defaultModels) { const loadedModel = loadedModels.find(m => m.type === defaultModel.type); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c15ad20a..8b62bb28 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -49,10 +49,17 @@ export interface MediaDbPluginSettings { openNoteInNewTab: boolean; useDefaultFrontMatter: boolean; /** When true, add an Obsidian `aliases` entry with an ASCII form of the title when it uses diacritics or letters like ø (e.g. Likbør → Likbor). */ - addNormalizeTitlesAsAlias: boolean; + autoTrackerAiringKey: string; + autoTrackerReleasedKey: string; enableTemplaterIntegration: boolean; imageDownload: boolean; imageFolder: string; + tmdbRegion: string; + enableAutoTagging: boolean; + autoTagEntities: string; + autoTagProperties: string; + enableWikiLinkParsing: boolean; + autoUpdateAiringMode: boolean; BoardgameGeekAPI_disabledMediaTypes: MediaType[]; ComicVineAPI_disabledMediaTypes: MediaType[]; @@ -355,10 +362,17 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { customDateFormat: 'L', openNoteInNewTab: true, useDefaultFrontMatter: true, - addNormalizeTitlesAsAlias: true, + autoTrackerAiringKey: 'airing', + autoTrackerReleasedKey: 'released', enableTemplaterIntegration: false, imageDownload: false, imageFolder: 'Media DB/images', + enableAutoTagging: false, + autoTagEntities: '', + autoTagProperties: '', + enableWikiLinkParsing: false, + autoUpdateAiringMode: false, + tmdbRegion: 'US', BoardgameGeekAPI_disabledMediaTypes: [], ComicVineAPI_disabledMediaTypes: [], @@ -738,6 +752,25 @@ export class MediaDbSettingTab extends PluginSettingTab { this.renderMediaTypeSection(panel, byType(MediaType.Season), mediaTypeApiMap, { sectionHeading: 'Season', }); + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + panel.createEl('h3', { text: 'Region' }); + const regionGroup = new SettingGroup(panel); + regionGroup.addSetting( + setting => + void setting + .setName('TMDB Region') + .setDesc('ISO-3166-1 region code for TMDB localized metadata (e.g., US, TR, GB). Default is US.') + .addText(text => + text + .setPlaceholder('US') + .setValue(this.plugin.settings.tmdbRegion) + .onChange(async value => { + this.plugin.settings.tmdbRegion = value; + await this.plugin.saveSettings(); + }), + ), + ); } display(): void { @@ -824,7 +857,7 @@ export class MediaDbSettingTab extends PluginSettingTab { "For more syntax, refer to format reference.
" + "Your current syntax looks like this: " + this.plugin.dateFormatter.getPreview() + - '', + "", ), ) .addText(cb => { @@ -864,7 +897,6 @@ export class MediaDbSettingTab extends PluginSettingTab { cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { this.plugin.settings.useDefaultFrontMatter = data; void this.plugin.saveSettings(); - // Redraw settings to display/remove the property mappings this.display(); }); }), @@ -874,9 +906,7 @@ export class MediaDbSettingTab extends PluginSettingTab { setting => void setting .setName('Enable Templater integration') - .setDesc( - 'Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.', - ) + .setDesc('Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.') .addToggle(cb => { cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { this.plugin.settings.enableTemplaterIntegration = data; @@ -920,27 +950,127 @@ export class MediaDbSettingTab extends PluginSettingTab { }), ); - generalGroup.addSetting( + panel.createEl('h3', { text: 'Auto-Tracker' }).style.marginTop = '1.5em'; + const autoTrackerGroup = new SettingGroup(panel); + + autoTrackerGroup.addSetting( setting => void setting - .setName('Add Normalized Titles as Alias') - .setDesc( - 'If the title contains non-ASCII characters, add a normalized ASCII version of the title in aliases.', - ) + .setName('Auto-Update Airing & Unreleased Media') + .setDesc('At startup, automatically searches background for any active medias with "released: false" or "airing: true" and updates them via API.') .addToggle(cb => { - cb.setValue(this.plugin.settings.addNormalizeTitlesAsAlias).onChange(data => { - this.plugin.settings.addNormalizeTitlesAsAlias = data; + cb.setValue(this.plugin.settings.autoUpdateAiringMode).onChange(data => { + this.plugin.settings.autoUpdateAiringMode = data; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTrackerGroup.addSetting( + setting => + void setting + .setName('Auto-Tracker "Airing" Property') + .setDesc('Property key to check if a media item is currently airing. Default is "airing".') + .addText(text => { + text.setValue(this.plugin.settings.autoTrackerAiringKey).onChange(data => { + this.plugin.settings.autoTrackerAiringKey = data.trim() || 'airing'; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTrackerGroup.addSetting( + setting => + void setting + .setName('Auto-Tracker "Released" Property') + .setDesc('Property key to check if a media item is unreleased. Default is "released".') + .addText(text => { + text.setValue(this.plugin.settings.autoTrackerReleasedKey).onChange(data => { + this.plugin.settings.autoTrackerReleasedKey = data.trim() || 'released'; void this.plugin.saveSettings(); }); }), ); + + panel.createEl('h3', { text: 'Auto-Tag Properties' }).style.marginTop = '1.5em'; + const autoTagGroup = new SettingGroup(panel); + + autoTagGroup.addSetting( + setting => + void setting + .setName('Enable Auto Tagging') + .setDesc('Feature to automatically sanitize properties into standard Obsidian tags.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableAutoTagging).onChange(data => { + this.plugin.settings.enableAutoTagging = data; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTagGroup.addSetting( + setting => + void setting + .setName('Auto-Tag whitelisted properties') + .setDesc('Comma separated list of property names. If a property in this list is present, its values will be sanitized and appended to the Obsidian native `tags` array.') + .addText(text => + text + .setPlaceholder('genres, platforms') + .setValue(this.plugin.settings.autoTagProperties) + .onChange(async value => { + this.plugin.settings.autoTagProperties = value; + await this.plugin.saveSettings(); + }), + ), + ); }); + // Render individual media type tabs + // Game tab + const renderGameTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { + this.renderMediaTypeSection(panel, setting, mediaTypeApiMap); + }; + + // Wiki tab — inject Wiki-Link settings + const renderWikiTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { + this.renderMediaTypeSection(panel, setting, mediaTypeApiMap, { + appendToSection: group => { + group.addSetting( + s => + void s + .setName('Wiki-Link parsing') + .setDesc('When enabled, properties listed below are formatted as Obsidian [[Wiki-Links]] across ALL media types globally. This complements the per-property wikilink checkbox in Property Mappings, which only affects that specific property.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableWikiLinkParsing).onChange(data => { + this.plugin.settings.enableWikiLinkParsing = data; + void this.plugin.saveSettings(); + }); + }), + ); + group.addSetting( + s => + void s + .setName('Wiki-Link properties') + .setDesc('Comma-separated property names to convert to [[Wiki-Links]] for ALL media types. Use this for custom or cross-type properties (e.g. storefront, launcher). For standard properties like genres or studio, the wikilink checkbox inside Property Mappings also works.') + .addTextArea(cb => { + cb.setPlaceholder('genres, storefront, category') + .setValue(this.plugin.settings.autoTagEntities) + .onChange(value => { + this.plugin.settings.autoTagEntities = value; + void this.plugin.saveSettings(); + }); + }), + ); + }, + }); + }; + addTab('api-keys', 'API keys', 'key', panel => { const apiKeyGroup = new SettingGroup(panel); this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', ApiSecretID.omdb); this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', ApiSecretID.tmdb); + this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', ApiSecretID.mobyGames); this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', ApiSecretID.giantBomb); this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', ApiSecretID.igdbClientId); @@ -1005,9 +1135,19 @@ export class MediaDbSettingTab extends PluginSettingTab { } const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); - addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { - this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); - }); + if (mediaType === MediaType.Game) { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + renderGameTab(panel, mediaTypeSetting); + }); + } else if (mediaType === MediaType.Wiki) { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + renderWikiTab(panel, mediaTypeSetting); + }); + } else { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); + }); + } } const validIds = new Set(tabEntries.map(t => t.id)); diff --git a/src/styles.css b/src/styles.css index c9c5edbb..5c0c2e6a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -361,3 +361,78 @@ small.media-db-plugin-list-text { width: 1px; white-space: nowrap; } + +/* ── Completion Modal ─────────────────────────────── */ +.mdb-completion-modal { + padding: 8px 4px 4px; + min-width: 280px; +} + +.mdb-completion-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.mdb-completion-icon { + font-size: 1.6em; + line-height: 1; +} + +.mdb-completion-title { + margin: 0; + font-size: 1.15em; + font-weight: 600; + color: var(--text-normal); +} + +.mdb-completion-stats { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; +} + +.mdb-completion-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 0; + border-bottom: 1px solid var(--background-modifier-border); +} + +.mdb-completion-label { + color: var(--text-muted); + font-size: 0.92em; +} + +.mdb-completion-value { + font-weight: 600; + color: var(--text-normal); + font-size: 0.95em; +} + +.mdb-stat-success { color: var(--color-green); } +.mdb-stat-error { color: var(--color-red); } +.mdb-stat-skipped { color: var(--text-muted); } + +.mdb-completion-notes { + margin-bottom: 20px; + padding: 8px 12px; + background: var(--background-secondary); + border-radius: var(--radius-s); + font-size: 0.88em; + color: var(--text-muted); +} + +.mdb-completion-notes p { + margin: 0; +} + +.mdb-completion-footer { + display: flex; + justify-content: flex-end; + padding-top: 12px; +} diff --git a/src/utils/AutoTrackerHelper.ts b/src/utils/AutoTrackerHelper.ts new file mode 100644 index 00000000..2e22ca5f --- /dev/null +++ b/src/utils/AutoTrackerHelper.ts @@ -0,0 +1,79 @@ +import { Notice, TFile, TFolder } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { CompletionModal } from 'src/modals/CompletionModal'; + +export class AutoTrackerHelper { + readonly plugin: MediaDbPlugin; + public isScanning: boolean = false; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async startBackgroundScan(silent: boolean = false, targetFolder?: TFolder): Promise { + if (this.isScanning) return; + this.isScanning = true; + this.plugin.refreshAutoTrackerRibbon(); + await this.runAutoUpdate(silent, targetFolder); + this.isScanning = false; + this.plugin.refreshAutoTrackerRibbon(); + } + + async runAutoUpdate(silent: boolean = false, targetFolder?: TFolder): Promise { + const allFiles = targetFolder + ? this.plugin.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(targetFolder.path)) + : this.plugin.app.vault.getMarkdownFiles(); + + const filesToUpdate: TFile[] = []; + + for (const file of allFiles) { + const metadata = this.plugin.getMetadataFromFileCache(file); + if (metadata && metadata.dataSource && metadata.id) { + const airingKey = this.plugin.settings.autoTrackerAiringKey; + const releasedKey = this.plugin.settings.autoTrackerReleasedKey; + if (metadata[airingKey] === true || metadata[releasedKey] === false) { + filesToUpdate.push(file); + } + } + } + + if (filesToUpdate.length === 0) { + if (!silent) { + new Notice('MDB Tracker | No airing or unreleased media found to update.'); + } + return; + } + + const noticeMsg = `MDB Tracker | Found ${filesToUpdate.length} ongoing/unreleased notes. Updating in background...`; + if (!silent) { + new Notice(noticeMsg); + } + console.log(noticeMsg); + + const startTime = Date.now(); + let successCount = 0; + let failCount = 0; + + for (const file of filesToUpdate) { + try { + await this.plugin.updateNote(file, true, false, silent); + successCount++; + } catch (e) { + console.warn(`MDB Tracker | Failed to auto-update ${file.path}: `, e); + failCount++; + } + // Sleep longer (1s) to be completely safe during background checks + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + new CompletionModal(this.plugin.app, { + title: 'Auto Tracker Complete', + icon: '🎯', + total: filesToUpdate.length, + success: successCount, + errors: failCount, + elapsedMs: Date.now() - startTime, + notes: failCount > 0 ? ['Some notes could not be updated. Check the console for details.'] : [], + }).open(); + } +} diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts index dcbaabb3..ffbe88dd 100644 --- a/src/utils/BulkImportHelper.ts +++ b/src/utils/BulkImportHelper.ts @@ -3,6 +3,7 @@ import { TFile } from 'obsidian'; import type MediaDbPlugin from 'src/main'; import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; import type { MediaTypeModel } from 'src/models/MediaTypeModel'; +import { CompletionModal } from 'src/modals/CompletionModal'; import { ModalResultCode } from './ModalHelper'; import { dateTimeToString, markdownTable } from './Utils'; @@ -27,6 +28,8 @@ export class BulkImportHelper { async import(folder: TFolder): Promise { const erroredFiles: BulkImportError[] = []; let canceled: boolean = false; + let successCount = 0; + const startTime = Date.now(); const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ selectedAPI: string; @@ -60,6 +63,8 @@ export class BulkImportHelper { const error = await this.importById(file, lookupValue, selectedAPI, appendContent); if (error) { erroredFiles.push(error); + } else { + successCount++; } } else if (lookupMethod === BulkImportLookupMethod.TITLE) { const error = await this.importByTitle(file, lookupValue, selectedAPI, appendContent); @@ -68,6 +73,8 @@ export class BulkImportHelper { canceled = true; } erroredFiles.push(error); + } else { + successCount++; } } else { erroredFiles.push({ filePath: file.path, error: `invalid lookup type` }); @@ -78,6 +85,18 @@ export class BulkImportHelper { if (erroredFiles.length > 0) { await this.createErroredFilesReport(erroredFiles); } + + const total = successCount + erroredFiles.length; + new CompletionModal(this.plugin.app, { + title: 'Bulk Import Complete', + icon: '📥', + total, + success: successCount, + errors: erroredFiles.filter(e => !e.canceled).length, + skipped: erroredFiles.filter(e => e.canceled).length, + elapsedMs: Date.now() - startTime, + notes: erroredFiles.length > 0 ? ['Error report saved to vault.'] : [], + }).open(); } private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { @@ -144,7 +163,7 @@ export class BulkImportHelper { const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - const fileContent = `# ${title}\n\n${markdownTable(table)}`; + const fileContent = markdownTable(table); await this.plugin.app.vault.create(filePath, fileContent); } } diff --git a/src/utils/BulkUpdateHelper.ts b/src/utils/BulkUpdateHelper.ts new file mode 100644 index 00000000..5dcc869b --- /dev/null +++ b/src/utils/BulkUpdateHelper.ts @@ -0,0 +1,53 @@ +import { TFolder, TFile, Notice } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { BulkUpdateConfirmModal } from 'src/modals/BulkUpdateConfirmModal'; +import { CompletionModal } from 'src/modals/CompletionModal'; + +export class BulkUpdateHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async updateFolder(folder: TFolder): Promise { + const mediaFiles = folder.children.filter((child): child is TFile => { + if (!(child instanceof TFile)) return false; + const metadata = this.plugin.getMetadataFromFileCache(child); + return Boolean(metadata && metadata.dataSource && metadata.id); + }); + + if (mediaFiles.length === 0) { + new Notice('MDB | No Media DB files found in this folder.'); + return; + } + + new BulkUpdateConfirmModal(this.plugin.app, async (silent: boolean) => { + new Notice(`MDB | Bulk updating ${mediaFiles.length} files. Please wait...`); + const startTime = Date.now(); + let successCount = 0; + let failCount = 0; + + for (const file of mediaFiles) { + try { + await this.plugin.updateNote(file, true, false, silent); + successCount++; + } catch (e) { + console.error(`MDB | Failed to bulk update ${file.path}: `, e); + failCount++; + } + await new Promise(resolve => setTimeout(resolve, 800)); + } + + new CompletionModal(this.plugin.app, { + title: 'Bulk Update Complete', + icon: '🔄', + total: mediaFiles.length, + success: successCount, + errors: failCount, + elapsedMs: Date.now() - startTime, + notes: failCount > 0 ? ['Some files could not be updated. Check the console for details.'] : [], + }).open(); + }).open(); + } +} diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 20961dd1..d0ca7fe1 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -22,7 +22,7 @@ export function containsOnlyLettersAndUnderscores(str: string): boolean { } export function replaceIllegalFileNameCharactersInString(string: string): string { - return string.replace(/[\\,#%&{}/*<>$"@.?]*/g, '').replace(/:+/g, ' -'); + return string.replace(/[\\/:"*?<>|]/g, '-'); } export function replaceTags(template: string, mediaTypeModel: MediaTypeModel, ignoreUndefined: boolean = false): string { @@ -41,7 +41,7 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin const obj = traverseMetaData(path, mediaTypeModel); - if (obj === undefined) { + if (obj === undefined || (path[path.length - 1] === 'year' && obj === 0)) { return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; } @@ -202,6 +202,7 @@ export interface CreateNoteOptions { attachFile?: TFile; openNote?: boolean; folder?: TFolder; + overwrite?: boolean; } /** Runtime in whole minutes (TMDB/OMDb/MAL). 0 when unknown. Parses legacy string frontmatter (e.g. "136 min", "2 hr 5 min"). */ diff --git a/src/utils/noteTypeSettings.ts b/src/utils/noteTypeSettings.ts index 1623514a..31b51d70 100644 --- a/src/utils/noteTypeSettings.ts +++ b/src/utils/noteTypeSettings.ts @@ -49,9 +49,6 @@ export function resolveMetadataTypeToMediaType( if (s === 'manga') { s = MediaType.ComicManga; } - if (s === 'band') { - s = MediaType.Artist; - } for (const mt of MEDIA_TYPES) { if (mt === s) { return mt; From 124c51170ca2c57b8c204aabab0e2a6634d315c1 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:35:31 +0300 Subject: [PATCH 29/60] bug fixes --- src/modals/BulkUpdateConfirmModal.ts | 2 +- src/modals/CompletionModal.ts | 2 +- src/models/BoardGameModel.ts | 2 +- src/models/BookModel.ts | 2 +- src/models/ComicMangaModel.ts | 2 +- src/models/GameModel.ts | 2 +- src/models/MediaTypeModel.ts | 5 ++++- src/models/MovieModel.ts | 2 +- src/models/MusicReleaseModel.ts | 2 +- src/models/SeriesModel.ts | 2 +- src/utils/Utils.ts | 6 +++++- 11 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/modals/BulkUpdateConfirmModal.ts b/src/modals/BulkUpdateConfirmModal.ts index 41c34555..5b9effe4 100644 --- a/src/modals/BulkUpdateConfirmModal.ts +++ b/src/modals/BulkUpdateConfirmModal.ts @@ -1,4 +1,4 @@ -import { App, Modal, Setting } from 'obsidian'; +import { type App, Modal, Setting } from 'obsidian'; export class BulkUpdateConfirmModal extends Modal { onSubmit: (silent: boolean) => void; diff --git a/src/modals/CompletionModal.ts b/src/modals/CompletionModal.ts index ac373dd6..233c3008 100644 --- a/src/modals/CompletionModal.ts +++ b/src/modals/CompletionModal.ts @@ -1,4 +1,4 @@ -import { App, Modal, ButtonComponent } from 'obsidian'; +import { type App, Modal, ButtonComponent } from 'obsidian'; export interface CompletionResult { /** Title shown in the modal header */ diff --git a/src/models/BoardGameModel.ts b/src/models/BoardGameModel.ts index e782137d..c65599b9 100644 --- a/src/models/BoardGameModel.ts +++ b/src/models/BoardGameModel.ts @@ -59,6 +59,6 @@ export class BoardGameModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + this.year + ')'; + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); } } diff --git a/src/models/BookModel.ts b/src/models/BookModel.ts index e75da93d..e2f52e48 100644 --- a/src/models/BookModel.ts +++ b/src/models/BookModel.ts @@ -59,6 +59,6 @@ export class BookModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + this.year + ') - ' + this.author; + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : '') + ' - ' + this.author; } } diff --git a/src/models/ComicMangaModel.ts b/src/models/ComicMangaModel.ts index ec47a959..0d4c64a3 100644 --- a/src/models/ComicMangaModel.ts +++ b/src/models/ComicMangaModel.ts @@ -75,6 +75,6 @@ export class ComicMangaModel extends MediaTypeModel { } getSummary(): string { - return this.title + ' (' + this.year + ')'; + return this.title + (this.year > 0 ? ` (${this.year})` : ''); } } diff --git a/src/models/GameModel.ts b/src/models/GameModel.ts index 7cde3425..90b972a2 100644 --- a/src/models/GameModel.ts +++ b/src/models/GameModel.ts @@ -63,6 +63,6 @@ export class GameModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + (this.year > 0 ? this.year : '') + ')'; + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); } } diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index c2c69761..e7e100b1 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -35,7 +35,10 @@ export abstract class MediaTypeModel { abstract getTags(): string[]; toMetaDataObject(): Record { - return { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; + const obj: Record = { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; + // year: 0 means "unknown" — write null so YAML shows blank (None) instead of 0 + if (obj['year'] === 0) obj['year'] = null; + return obj; } getWithOutUserData(): Record { diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index ddd15c01..08b7a1b9 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -83,6 +83,6 @@ export class MovieModel extends MediaTypeModel { } getSummary(): string { - return this.englishTitle + ' (' + (this.year > 0 ? this.year : '') + ')'; + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); } } diff --git a/src/models/MusicReleaseModel.ts b/src/models/MusicReleaseModel.ts index d5e236bd..3c3d6634 100644 --- a/src/models/MusicReleaseModel.ts +++ b/src/models/MusicReleaseModel.ts @@ -62,7 +62,7 @@ export class MusicReleaseModel extends MediaTypeModel { } getSummary(): string { - let summary = this.title + ' (' + this.year + ')'; + let summary = this.title + (this.year > 0 ? ` (${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 2272b697..4ca1662b 100644 --- a/src/models/SeriesModel.ts +++ b/src/models/SeriesModel.ts @@ -81,6 +81,6 @@ export class SeriesModel extends MediaTypeModel { } getSummary(): string { - return this.title + ' (' + (this.year > 0 ? this.year : '') + ')'; + return this.title + (this.year > 0 ? ` (${this.year})` : ''); } } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index d0ca7fe1..b4a16b99 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -41,9 +41,13 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefin const obj = traverseMetaData(path, mediaTypeModel); - if (obj === undefined || (path[path.length - 1] === 'year' && obj === 0)) { + if (obj === undefined) { return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; } + // year: 0 means "unknown" — return empty string so filename templates stay clean (e.g. "Title ()") + if (path[path.length - 1] === 'year' && obj === 0) { + return ''; + } // eslint-disable-next-line @typescript-eslint/no-base-to-string return obj?.toString() ?? 'null'; From 6978f367f058dad662829a8c7dbde2663b862c72 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:39:25 +0300 Subject: [PATCH 30/60] Delete package-lock.json This project uses Bun and has a bun.lockb, so no need for package-lock.json --- package-lock.json | 5719 --------------------------------------------- 1 file changed, 5719 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6d29e3c8..00000000 --- a/package-lock.json +++ /dev/null @@ -1,5719 +0,0 @@ -{ - "name": "obsidian-media-db-plugin", - "version": "0.8.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "obsidian-media-db-plugin", - "version": "0.8.0", - "license": "GPL-3.0", - "devDependencies": { - "@lemons_dev/parsinom": "^0.0.12", - "@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": "^1.12.3", - "openapi-fetch": "^0.14.1", - "openapi-typescript": "^7.10.1", - "prettier": "^3.8.1", - "solid-js": "^1.9.3", - "string-argv": "^0.3.2", - "tslib": "^2.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", - "vite": "^6.0.5", - "vite-plugin-banner": "^0.8.1", - "vite-plugin-solid": "^2.11.0", - "vite-plugin-static-copy": "^3.2.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@codemirror/state": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", - "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", - "dev": true, - "peer": true, - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.38.6", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", - "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", - "dev": true, - "peer": true, - "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lemons_dev/parsinom": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@lemons_dev/parsinom/-/parsinom-0.0.12.tgz", - "integrity": "sha512-i6oUfQfhw4ZStScMpPHy8ZmLrkn29RX/uK1SBKSKPuH0w9vOFQjZ0O4ev1hdk0K/eU196mk9mAlI1bjbO4n4sQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "dev": true, - "peer": true - }, - "node_modules/@redocly/ajv": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.2.tgz", - "integrity": "sha512-rcbDZOfXAgGEJeJ30aWCVVJvxV9ooevb/m1/SFblO2qHs4cqTk178gx7T/vdslf57EA4lTofrwsq5K8rxK9g+g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@redocly/config": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", - "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", - "dev": true - }, - "node_modules/@redocly/openapi-core": { - "version": "1.34.6", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", - "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", - "dev": true, - "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.22.0", - "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.5", - "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "minimatch": "^5.0.1", - "pluralize": "^8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/bun": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", - "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", - "dev": true, - "dependencies": { - "bun-types": "1.3.11" - } - }, - "node_modules/@types/codemirror": { - "version": "5.60.8", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", - "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/tern": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", - "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/tern": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", - "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", - "dev": true, - "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "debug": "^4.4.3", - "minimatch": "^9.0.5", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-plugin-jsx-dom-expressions": { - "version": "0.40.6", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.6.tgz", - "integrity": "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "7.18.6", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.20.7", - "html-entities": "2.3.3", - "parse5": "^7.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.20.12" - } - }, - "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/babel-preset-solid": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", - "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", - "dev": true, - "dependencies": { - "babel-plugin-jsx-dom-expressions": "^0.40.6" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "solid-js": "^1.9.12" - }, - "peerDependenciesMeta": { - "solid-js": { - "optional": true - } - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", - "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/builtin-modules": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", - "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bun-types": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", - "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/change-case": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", - "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", - "dev": true - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.328", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", - "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", - "dev": true - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-only-warn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.1.0.tgz", - "integrity": "sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", - "dev": true, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iso-639-2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/iso-639-2/-/iso-639-2-3.0.2.tgz", - "integrity": "sha512-tna50aWwcGTIn81S9MzD1NSovHYTpFgmPVszHiLF5Vg/xmXAJ9XAkMOB9a8TH9Vi7qwf/x/8NJy2F+lM5OEwAw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge-anything": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", - "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", - "dev": true, - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obsidian": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", - "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", - "dev": true, - "dependencies": { - "@types/codemirror": "5.60.8", - "moment": "2.29.4" - }, - "peerDependencies": { - "@codemirror/state": "6.5.0", - "@codemirror/view": "6.38.6" - } - }, - "node_modules/openapi-fetch": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.1.tgz", - "integrity": "sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==", - "dev": true, - "dependencies": { - "openapi-typescript-helpers": "^0.0.15" - } - }, - "node_modules/openapi-typescript": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.1.tgz", - "integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==", - "dev": true, - "dependencies": { - "@redocly/openapi-core": "^1.34.5", - "ansi-colors": "^4.1.3", - "change-case": "^5.4.4", - "parse-json": "^8.3.0", - "supports-color": "^10.2.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "openapi-typescript": "bin/cli.js" - }, - "peerDependencies": { - "typescript": "^5.x" - } - }, - "node_modules/openapi-typescript-helpers": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", - "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", - "dev": true - }, - "node_modules/openapi-typescript/node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/seroval": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.1.tgz", - "integrity": "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/seroval-plugins": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.1.tgz", - "integrity": "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "seroval": "^1.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/solid-js": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", - "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", - "dev": true, - "dependencies": { - "csstype": "^3.1.0", - "seroval": "~1.5.0", - "seroval-plugins": "~1.5.0" - } - }, - "node_modules/solid-refresh": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz", - "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.6", - "@babel/helper-module-imports": "^7.22.15", - "@babel/types": "^7.23.6" - }, - "peerDependencies": { - "solid-js": "^1.3" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "dev": true, - "peer": true - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-banner": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/vite-plugin-banner/-/vite-plugin-banner-0.8.1.tgz", - "integrity": "sha512-0+gGguHk3MH0HvzMSOCJC6fGgH4+jtY9KlKVZh+hwwE+PBkGVzY8xe657JL74vEgbeUJD37XjVqTrmve8XvZBQ==", - "dev": true - }, - "node_modules/vite-plugin-solid": { - "version": "2.11.11", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.11.tgz", - "integrity": "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.3", - "@types/babel__core": "^7.20.4", - "babel-preset-solid": "^1.8.4", - "merge-anything": "^5.1.7", - "solid-refresh": "^0.6.3", - "vitefu": "^1.0.4" - }, - "peerDependencies": { - "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", - "solid-js": "^1.7.2", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@testing-library/jest-dom": { - "optional": true - } - } - }, - "node_modules/vite-plugin-static-copy": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz", - "integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==", - "dev": true, - "dependencies": { - "chokidar": "^3.6.0", - "p-map": "^7.0.4", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.15" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/sapphi-red" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", - "dev": true, - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true, - "peer": true - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", - "dev": true - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} From 56dd5e1950ffb8acad838b598a0924a59b8583d0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:59:49 +0300 Subject: [PATCH 31/60] fix Fixed an issue where games with both franchise and collection data (e.g., Aliens vs. Predator) would append multiple overlapping series tags. The API now correctly prioritizes the main Franchise name, falling back to Collection only if Franchise data is unavailable. --- src/api/apis/IGDBAPI.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index bd88719a..51306143 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -108,9 +108,14 @@ export class IGDBAPI extends APIModel { const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; let combinedSeries: string[] = []; - if (result.collection?.name) combinedSeries.push(result.collection.name); - result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); + // Öncelik 1: Franchise (Ana marka) result.franchises?.forEach(f => { if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); }); + + // Öncelik 2: Franchise yoksa Collection (Seri) fallback'i + if (combinedSeries.length === 0) { + if (result.collection?.name) combinedSeries.push(result.collection.name); + result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); + } return new GameModel({ type: MediaType.Game, title: result.name, englishTitle: result.name, From 1026393de74f5d44c608832a15d78a8aca1b8b74 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:33:05 +0300 Subject: [PATCH 32/60] fixes Added markdown report generation for BulkUpdateHelper, AutoTrackerHelper, and main.ts (Download Images). Failed files are now logged directly to /MDB - error report .md instead of just logging to the console. Filename formatting standardized to match the existing Bulk Import feature layout. Modified downloadImagesInFile to return rich {success, error, skipped} objects for exact failure tracking. --- api/APIManager.ts | 113 + api/APIModel.ts | 46 + api/GeniusClient.ts | 86 + api/SpotifyClient.ts | 145 + api/apis/BoardGameGeekAPI.ts | 151 + api/apis/ComicVineAPI.ts | 121 + api/apis/GiantBombAPI.ts | 168 + api/apis/IGDBAPI.ts | 137 + api/apis/MALAPI.ts | 229 + api/apis/MALAPIManga.ts | 161 + api/apis/MobyGamesAPI.ts | 122 + api/apis/MusicBrainzAPI.ts | 328 + api/apis/MusicBrainzArtistAPI.ts | 249 + api/apis/OMDbAPI.ts | 291 + api/apis/OpenLibraryAPI.ts | 161 + api/apis/RAWGAPI.ts | 68 + api/apis/SteamAPI.ts | 245 + api/apis/TMDBMovieAPI.ts | 202 + api/apis/TMDBSeasonAPI.ts | 277 + api/apis/TMDBSeriesAPI.ts | 168 + api/apis/VNDBAPI.ts | 267 + api/apis/WikipediaAPI.ts | 112 + api/geniusLyricsExtract.ts | 87 + api/musicBrainzConstants.ts | 19 + api/schemas/GiantBomb.json | 11226 +++++++++ api/schemas/GiantBomb.ts | 6454 ++++++ api/schemas/MALAPI.ts | 6761 ++++++ api/schemas/OpenLibrary.json | 602 + api/schemas/OpenLibrary.ts | 578 + api/schemas/TMDB.ts | 22832 +++++++++++++++++++ main.ts | 1266 + modals/BulkUpdateConfirmModal.ts | 39 + modals/CompletionModal.ts | 85 + modals/ConfirmOverwriteModal.ts | 45 + modals/MediaDbAdvancedSearchModal.ts | 133 + modals/MediaDbBulkImportModal.ts | 134 + modals/MediaDbIdSearchModal.ts | 119 + modals/MediaDbPreviewModal.ts | 86 + modals/MediaDbSearchModal.ts | 145 + modals/MediaDbSearchResultModal.ts | 66 + modals/MediaDbSeasonSelectModal.ts | 50 + modals/PropertyMappingModal.ts | 59 + modals/SelectModal.ts | 176 + modals/SelectModalElement.ts | 88 + models/ArtistModel.ts | 63 + models/BoardGameModel.ts | 64 + models/BookModel.ts | 64 + models/ComicMangaModel.ts | 80 + models/GameModel.ts | 68 + models/MediaTypeModel.ts | 49 + models/MovieModel.ts | 88 + models/MusicReleaseModel.ts | 69 + models/SeasonModel.ts | 80 + models/SeriesModel.ts | 86 + models/SongModel.ts | 84 + models/WikiModel.ts | 52 + settings/Icon.tsx | 24 + settings/PropertyMapper.ts | 209 + settings/PropertyMapping.ts | 277 + settings/PropertyMappingModelComponent.tsx | 128 + settings/Settings.ts | 1160 + settings/apiSecretsHelper.ts | 28 + settings/suggesters/FileSuggest.ts | 17 + settings/suggesters/FolderSuggest.ts | 17 + styles.css | 457 +- utils/AutoTrackerHelper.ts | 90 + utils/BulkImportHelper.ts | 169 + utils/BulkUpdateHelper.ts | 64 + utils/DateFormatter.ts | 65 + utils/IconList.ts | 1185 + utils/IllegalFilenameCharactersList.ts | 14 + utils/MediaType.ts | 13 + utils/MediaTypeManager.ts | 179 + utils/ModalHelper.ts | 537 + utils/Utils.ts | 387 + utils/normalizeTitleForAlias.ts | 52 + utils/noteTypeSettings.ts | 63 + 77 files changed, 60859 insertions(+), 20 deletions(-) create mode 100644 api/APIManager.ts create mode 100644 api/APIModel.ts create mode 100644 api/GeniusClient.ts create mode 100644 api/SpotifyClient.ts create mode 100644 api/apis/BoardGameGeekAPI.ts create mode 100644 api/apis/ComicVineAPI.ts create mode 100644 api/apis/GiantBombAPI.ts create mode 100644 api/apis/IGDBAPI.ts create mode 100644 api/apis/MALAPI.ts create mode 100644 api/apis/MALAPIManga.ts create mode 100644 api/apis/MobyGamesAPI.ts create mode 100644 api/apis/MusicBrainzAPI.ts create mode 100644 api/apis/MusicBrainzArtistAPI.ts create mode 100644 api/apis/OMDbAPI.ts create mode 100644 api/apis/OpenLibraryAPI.ts create mode 100644 api/apis/RAWGAPI.ts create mode 100644 api/apis/SteamAPI.ts create mode 100644 api/apis/TMDBMovieAPI.ts create mode 100644 api/apis/TMDBSeasonAPI.ts create mode 100644 api/apis/TMDBSeriesAPI.ts create mode 100644 api/apis/VNDBAPI.ts create mode 100644 api/apis/WikipediaAPI.ts create mode 100644 api/geniusLyricsExtract.ts create mode 100644 api/musicBrainzConstants.ts create mode 100644 api/schemas/GiantBomb.json create mode 100644 api/schemas/GiantBomb.ts create mode 100644 api/schemas/MALAPI.ts create mode 100644 api/schemas/OpenLibrary.json create mode 100644 api/schemas/OpenLibrary.ts create mode 100644 api/schemas/TMDB.ts create mode 100644 main.ts create mode 100644 modals/BulkUpdateConfirmModal.ts create mode 100644 modals/CompletionModal.ts create mode 100644 modals/ConfirmOverwriteModal.ts create mode 100644 modals/MediaDbAdvancedSearchModal.ts create mode 100644 modals/MediaDbBulkImportModal.ts create mode 100644 modals/MediaDbIdSearchModal.ts create mode 100644 modals/MediaDbPreviewModal.ts create mode 100644 modals/MediaDbSearchModal.ts create mode 100644 modals/MediaDbSearchResultModal.ts create mode 100644 modals/MediaDbSeasonSelectModal.ts create mode 100644 modals/PropertyMappingModal.ts create mode 100644 modals/SelectModal.ts create mode 100644 modals/SelectModalElement.ts create mode 100644 models/ArtistModel.ts create mode 100644 models/BoardGameModel.ts create mode 100644 models/BookModel.ts create mode 100644 models/ComicMangaModel.ts create mode 100644 models/GameModel.ts create mode 100644 models/MediaTypeModel.ts create mode 100644 models/MovieModel.ts create mode 100644 models/MusicReleaseModel.ts create mode 100644 models/SeasonModel.ts create mode 100644 models/SeriesModel.ts create mode 100644 models/SongModel.ts create mode 100644 models/WikiModel.ts create mode 100644 settings/Icon.tsx create mode 100644 settings/PropertyMapper.ts create mode 100644 settings/PropertyMapping.ts create mode 100644 settings/PropertyMappingModelComponent.tsx create mode 100644 settings/Settings.ts create mode 100644 settings/apiSecretsHelper.ts create mode 100644 settings/suggesters/FileSuggest.ts create mode 100644 settings/suggesters/FolderSuggest.ts create mode 100644 utils/AutoTrackerHelper.ts create mode 100644 utils/BulkImportHelper.ts create mode 100644 utils/BulkUpdateHelper.ts create mode 100644 utils/DateFormatter.ts create mode 100644 utils/IconList.ts create mode 100644 utils/IllegalFilenameCharactersList.ts create mode 100644 utils/MediaType.ts create mode 100644 utils/MediaTypeManager.ts create mode 100644 utils/ModalHelper.ts create mode 100644 utils/Utils.ts create mode 100644 utils/normalizeTitleForAlias.ts create mode 100644 utils/noteTypeSettings.ts diff --git a/api/APIManager.ts b/api/APIManager.ts new file mode 100644 index 00000000..bcdd5401 --- /dev/null +++ b/api/APIManager.ts @@ -0,0 +1,113 @@ +import { Notice } from 'obsidian'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from '../utils/MediaType'; +import { + isMusicBrainzFamilyDataSource, + musicBrainzRegisteredApiName, + MUSICBRAINZ_NOTE_DATA_SOURCE, +} from './musicBrainzConstants'; +import type { APIModel } from './APIModel'; + +export class APIManager { + apis: APIModel[]; + + constructor() { + this.apis = []; + } + + /** + * Queries the basic info for one query string and multiple APIs. + * + * @param query + * @param apisToQuery + */ + async query(query: string, apisToQuery: string[]): Promise { + console.debug(`MDB | api manager queried with "${query}"`); + + const promises = this.apis + .filter(api => apisToQuery.contains(api.apiName)) + .map(async api => { + try { + return await api.searchByTitle(query); + } catch (e) { + new Notice(`Error querying ${api.apiName}: ${e}`); + console.warn(e); + + return []; + } + }); + + return (await Promise.all(promises)).flat(); + } + + /** + * Queries detailed information for a MediaTypeModel. + * + * @param item + */ + async queryDetailedInfo(item: MediaTypeModel): Promise { + 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 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, mediaType?: MediaType): Promise { + const trimmed = apiName.trim(); + const effectiveApiName = + trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) + ? MUSICBRAINZ_NOTE_DATA_SOURCE + : trimmed || apiName; + + if (isMusicBrainzFamilyDataSource(effectiveApiName) && mediaType !== undefined) { + const registeredName = musicBrainzRegisteredApiName(mediaType); + if (registeredName) { + const api = this.getApiByName(registeredName); + if (api) { + try { + return await api.getById(id); + } catch (e) { + new Notice(`Error querying ${api.apiName}: ${e}`); + console.warn(e); + + return undefined; + } + } + } + } + + for (const api of this.apis) { + if (api.apiName === effectiveApiName) { + try { + return api.getById(id); + } catch (e) { + new Notice(`Error querying ${api.apiName}: ${e}`); + console.warn(e); + + return undefined; + } + } + } + + return undefined; + } + + getApiByName(name: string): APIModel | undefined { + for (const api of this.apis) { + if (api.apiName === name) { + return api; + } + } + + return undefined; + } + + registerAPI(api: APIModel): void { + this.apis.push(api); + } +} diff --git a/api/APIModel.ts b/api/APIModel.ts new file mode 100644 index 00000000..50e6d72c --- /dev/null +++ b/api/APIModel.ts @@ -0,0 +1,46 @@ +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from '../utils/MediaType'; + +export abstract class APIModel { + apiName!: string; + apiUrl!: string; + apiDescription!: string; + types!: MediaType[]; + plugin!: MediaDbPlugin; + + /** + * This function should query the api and return a list of matches. The matches should be capped at 20. + * + * @param title the title to query for + */ + abstract searchByTitle(title: string): Promise; + + abstract getById(id: string): Promise; + + abstract getDisabledMediaTypes(): MediaType[]; + + hasType(type: MediaType): boolean { + const disabledMediaTypes = this.getDisabledMediaTypes(); + return this.types.includes(type) && !disabledMediaTypes.includes(type); + } + + hasTypeOverlap(types: MediaType[]): boolean { + return types.some(type => this.hasType(type)); + } + + /** + * Returns the wiki-link string for a given property value. + * Subclasses can override this to apply API-specific file name templates + * (e.g. using an Artist file name for artist links). + * + * @param _property the property key (e.g. 'artists', 'albumTitle') + * @param value the raw string value to wrap + * @param _obj the full metadata object (for context) + * @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/') + */ + wikilinkValueFor(_property: string, value: string, _obj: Record, folderPrefix: string): string { + const clean = value.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; + return `[[${folderPrefix}${clean}|${clean}]]`; + } +} diff --git a/api/GeniusClient.ts b/api/GeniusClient.ts new file mode 100644 index 00000000..4b228177 --- /dev/null +++ b/api/GeniusClient.ts @@ -0,0 +1,86 @@ +import { requestUrl } from 'obsidian'; + +import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; + +import { extractLyricsFromGeniusHtml } from './geniusLyricsExtract'; + +interface GeniusSearchHit { + result: { + id: number; + title: string; + url: string; + primary_artist: { name: string }; + }; +} + +interface GeniusSearchResponse { + response: { + hits: GeniusSearchHit[]; + }; +} + +export { extractLyricsFromGeniusHtml }; + +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 { + 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); + } +} diff --git a/api/SpotifyClient.ts b/api/SpotifyClient.ts new file mode 100644 index 00000000..48e87bae --- /dev/null +++ b/api/SpotifyClient.ts @@ -0,0 +1,145 @@ +import { requestUrl } from 'obsidian'; + +import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; + +interface SpotifyTokenResponse { + access_token: string; + expires_in: number; + token_type: string; +} + +interface SpotifySearchResponse { + tracks?: { + items: { external_urls?: { spotify?: string } }[]; + }; +} + +function spotifyTrackArtistQuery(trackTitle: string, artistName: string): string { + const clean = (s: string) => s.trim().replace(/"/g, ' ').replace(/\s+/g, ' '); + const t = clean(trackTitle); + const a = clean(artistName); + if (!t) { + return ''; + } + if (!a) { + return `track:"${t}"`; + } + return `track:"${t}" artist:"${a}"`; +} + +export class SpotifyClient { + private readonly clientId: string; + private readonly clientSecret: string; + private readonly userAgent: string; + private accessToken: string | null = null; + private tokenExpiresAtMs = 0; + + constructor(clientId: string | undefined, clientSecret: string | undefined) { + this.clientId = (clientId ?? '').trim(); + this.clientSecret = (clientSecret ?? '').trim(); + this.userAgent = `${pluginName}/${mediaDbVersion} (${contactEmail})`; + } + + isConfigured(): boolean { + return Boolean(this.clientId && this.clientSecret); + } + + private async refreshAccessToken(): Promise { + if (!this.isConfigured()) { + return null; + } + const basic = btoa(`${this.clientId}:${this.clientSecret}`); + const res = await requestUrl({ + url: 'https://accounts.spotify.com/api/token', + method: 'POST', + throw: false, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basic}`, + 'User-Agent': this.userAgent, + }, + body: 'grant_type=client_credentials', + }); + if (res.status !== 200) { + console.warn(`MDB | Spotify token request returned ${res.status}`); + this.accessToken = null; + this.tokenExpiresAtMs = 0; + return null; + } + const data = res.json as SpotifyTokenResponse; + if (!data.access_token) { + return null; + } + this.accessToken = data.access_token; + const ttlMs = (data.expires_in ?? 3600) * 1000; + this.tokenExpiresAtMs = Date.now() + ttlMs - 60_000; + return this.accessToken; + } + + private async getAccessToken(): Promise { + if (!this.isConfigured()) { + return null; + } + const now = Date.now(); + if (this.accessToken && now < this.tokenExpiresAtMs) { + return this.accessToken; + } + return this.refreshAccessToken(); + } + + /** + * Search for a track and return the first result's open.spotify.com URL, or ''. + */ + async searchFirstTrackUrl(trackTitle: string, artistName: string): Promise { + const q = spotifyTrackArtistQuery(trackTitle, artistName); + if (!q) { + return ''; + } + let token = await this.getAccessToken(); + if (!token) { + console.warn('MDB | Spotify search fetch skipped: could not obtain access token'); + return ''; + } + + const params = new URLSearchParams({ q, type: 'track', limit: '1' }); + const url = `https://api.spotify.com/v1/search?${params.toString()}`; + console.log(`MDB | Spotify search fetch: ${url}`); + let res = await requestUrl({ + url, + method: 'GET', + throw: false, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': this.userAgent, + }, + }); + + if (res.status === 401) { + this.accessToken = null; + this.tokenExpiresAtMs = 0; + token = await this.refreshAccessToken(); + if (!token) { + return ''; + } + console.log(`MDB | Spotify search fetch (retry after 401): ${url}`); + res = await requestUrl({ + url, + method: 'GET', + throw: false, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': this.userAgent, + }, + }); + } + + if (res.status !== 200) { + console.warn(`MDB | Spotify search returned ${res.status}`); + return ''; + } + + const data = res.json as SpotifySearchResponse; + const link = data.tracks?.items?.[0]?.external_urls?.spotify; + return typeof link === 'string' ? link : ''; + } +} diff --git a/api/apis/BoardGameGeekAPI.ts b/api/apis/BoardGameGeekAPI.ts new file mode 100644 index 00000000..bbe251d5 --- /dev/null +++ b/api/apis/BoardGameGeekAPI.ts @@ -0,0 +1,151 @@ +import { requestUrl } from 'obsidian'; +import { BoardGameModel } from 'src/models/BoardGameModel'; +import type MediaDbPlugin from '../../main'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { coerceYear } from '../../utils/Utils'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +// sadly no open api schema available + +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://boardgamegeek.com/xmlapi/'; + this.types = [MediaType.BoardGame]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); + if (!bggKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; + const fetchData = await requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${bggKey}`, + }, + }); + + 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 = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + + // console.debug(response); + + const ret: MediaTypeModel[] = []; + + for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { + const id = boardgame.attributes.getNamedItem('objectid')?.value; + const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined; + const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; + + ret.push( + new BoardGameModel({ + dataSource: this.apiName, + id, + title, + englishTitle: title, + year: coerceYear(year), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); + if (!bggKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; + const fetchData = await requestUrl({ + url: searchUrl, + headers: { + Authorization: `Bearer ${bggKey}`, + }, + }); + + 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 = fetchData.text; + const response = new window.DOMParser().parseFromString(data, 'text/xml'); + // console.debug(response); + + const boardgame = response.querySelector('boardgame'); + if (!boardgame) { + throw Error(`MDB | Received invalid data from ${this.apiName}.`); + } + + const title = boardgame.querySelector('name[primary=true]')?.textContent; + const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; + const image = boardgame.querySelector('image')?.textContent ?? undefined; + const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); + const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')) + .map(n => n.textContent) + .filter(n => n !== null); + const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); + const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); + const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); + const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; + const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')) + .map(n => n.textContent) + .filter(n => n !== null); + + return new BoardGameModel({ + title: title ?? undefined, + englishTitle: title ?? undefined, + year: year === '0' ? 0 : coerceYear(year), + dataSource: this.apiName, + url: `https://boardgamegeek.com/boardgame/${id}`, + id: id, + + genres: genres, + onlineRating: onlineRating, + complexityRating: complexityRating, + minPlayers: minPlayers, + maxPlayers: maxPlayers, + playtime: playtime, + publishers: publishers, + image: image, + + released: true, + + userData: { + played: false, + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; + } +} diff --git a/api/apis/ComicVineAPI.ts b/api/apis/ComicVineAPI.ts new file mode 100644 index 00000000..1bfa3daa --- /dev/null +++ b/api/apis/ComicVineAPI.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ + +import { requestUrl } from 'obsidian'; +import { ComicMangaModel } from 'src/models/ComicMangaModel'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +// sadly no open api schema available + +export class ComicVineAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'ComicVineAPI'; + this.apiDescription = 'A free API for comic books.'; + this.apiUrl = 'https://comicvine.gamespot.com/api'; + this.types = [MediaType.ComicManga]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/search/?api_key=${apiKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + // console.debug(fetchData); + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.results) { + ret.push( + new ComicMangaModel({ + title: result.name, + englishTitle: result.name, + year: coerceYear(result.start_year), + dataSource: this.apiName, + id: `4050-${result.id}`, + publishers: result.publisher?.name, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${apiKey}&format=json`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + const result = data.results; + + const authors = result.people as + | { + name: string; + }[] + | undefined; + + return new ComicMangaModel({ + type: MediaType.ComicManga, + title: result.name, + englishTitle: result.name, + alternateTitles: result.aliases, + plot: result.deck, + year: coerceYear(result.start_year), + dataSource: this.apiName, + url: result.site_detail_url, + id: `4050-${result.id}`, + + authors: authors?.map(x => x.name), + chapters: result.count_of_issues, + image: result.image?.original_url, + + released: true, + publishers: result.publisher?.name, + publishedFrom: result.start_year, + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.ComicVineAPI_disabledMediaTypes; + } +} diff --git a/api/apis/GiantBombAPI.ts b/api/apis/GiantBombAPI.ts new file mode 100644 index 00000000..3aba1336 --- /dev/null +++ b/api/apis/GiantBombAPI.ts @@ -0,0 +1,168 @@ +import createClient from 'openapi-fetch'; +import { coerceYear, obsidianFetch } from 'src/utils/Utils'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/GiantBomb'; + +export class GiantBombAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'GiantBombAPI'; + this.apiDescription = 'A free API for games.'; + this.apiUrl = 'https://www.giantbomb.com/api'; + this.types = [MediaType.Game]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); + const response = await client.GET('/games', { + params: { + query: { + api_key: apiKey, + filter: `name:${title}`, + format: 'json', + limit: 20, + }, + }, + fetch: obsidianFetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status === 429) { + throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data?.results; + + const ret: MediaTypeModel[] = []; + for (const result of data ?? []) { + const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; + + ret.push( + new GameModel({ + title: result.name, + englishTitle: result.name, + year: coerceYear(year), + dataSource: this.apiName, + id: result.guid?.toString(), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); + const response = await client.GET('/game/{guid}', { + params: { + path: { + guid: id, + }, + query: { + api_key: apiKey, + format: 'json', + }, + }, + fetch: obsidianFetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status === 429) { + throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const result = response.data?.results; + + if (!result) { + throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); + } + + console.log(result); + + // sadly the only OpenAPI definition I could find doesn't have the right types + const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; + const developers = result.developers as + | { + name: string; + }[] + | undefined; + const publishers = result.publishers as + | { + name: string; + }[] + | undefined; + const genres = result.genres as + | { + name: string; + }[] + | undefined; + const image = result.image as + | { + small_url: string; + medium_url: string; + super_url: string; + } + | undefined; + + return new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: coerceYear(year), + dataSource: this.apiName, + url: result.site_detail_url, + id: result.guid?.toString(), + developers: developers?.map(x => x.name), + publishers: publishers?.map(x => x.name), + genres: genres?.map(x => x.name), + onlineRating: 0, + image: image?.super_url, + + released: true, + releaseDate: result.original_release_date, + + userData: { + played: false, + + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.GiantBombAPI_disabledMediaTypes; + } +} diff --git a/api/apis/IGDBAPI.ts b/api/apis/IGDBAPI.ts new file mode 100644 index 00000000..51306143 --- /dev/null +++ b/api/apis/IGDBAPI.ts @@ -0,0 +1,137 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +interface IGDBCover { url: string; } +interface IGDBGenre { name: string; } +interface IGDBCompany { name: string; } +interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } +interface IGDBPlatform { name: string; } +interface IGDBGameMode { name: string; } +interface IGDBCollection { name: string; } +interface IGDBGame { + id: number; name: string; cover?: IGDBCover; first_release_date?: number; + summary?: string; total_rating?: number; url?: string; + genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; + platforms?: IGDBPlatform[]; game_modes?: IGDBGameMode[]; + collection?: IGDBCollection; collections?: IGDBCollection[]; franchises?: IGDBCollection[]; +} +interface TwitchAuthResponse { access_token: string; expires_in: number; } + +export class IGDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + private accessToken: string = ''; + private tokenExpiry: number = 0; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'IGDBAPI'; + this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; + this.apiUrl = 'https://api.igdb.com/v4'; + this.types = [MediaType.Game]; + } + + private async getAuthToken(): Promise { + const currentTime = Date.now(); + if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; + + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); + const clientSecret = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientSecret); + if (!clientId || !clientSecret) { + throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); + } + console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); + const response = await requestUrl({ + url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, + method: 'POST', + }); + if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); + const data = response.json as TwitchAuthResponse; + this.accessToken = data.access_token; + this.tokenExpiry = currentTime + (data.expires_in * 1000) - 60000; + return this.accessToken; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + const token = await this.getAuthToken(); + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); + const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; + const response = await requestUrl({ + url: `${this.apiUrl}/games`, method: 'POST', + headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + body: queryBody, + }); + if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); + + const data = response.json as IGDBGame[]; + return data.map(result => { + const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; + return new GameModel({ + type: MediaType.Game, title: result.name, englishTitle: result.name, year: coerceYear(year), + dataSource: this.apiName, id: result.id.toString(), image: image + }); + }); + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + const token = await this.getAuthToken(); + const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); + const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher, platforms.name, game_modes.name, collection.name, collections.name, franchises.name; where id = ${id};`; + const response = await requestUrl({ + url: `${this.apiUrl}/games`, method: 'POST', + headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + body: queryBody, + }); + if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); + + const data = response.json as IGDBGame[]; + if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); + const result = data[0]; + + const developers: string[] = []; + const publishers: string[] = []; + result.involved_companies?.forEach(c => { + if (c.developer) developers.push(c.company.name); + if (c.publisher) publishers.push(c.company.name); + }); + const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; + const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; + + let combinedSeries: string[] = []; + // Öncelik 1: Franchise (Ana marka) + result.franchises?.forEach(f => { if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); }); + + // Öncelik 2: Franchise yoksa Collection (Seri) fallback'i + if (combinedSeries.length === 0) { + if (result.collection?.name) combinedSeries.push(result.collection.name); + result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); + } + + return new GameModel({ + type: MediaType.Game, title: result.name, englishTitle: result.name, + year: coerceYear( + result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0, + ), + dataSource: this.apiName, url: result.url, id: result.id.toString(), + summary: result.summary ?? '', series: combinedSeries, + gameModes: result.game_modes?.map(g => g.name) || [], platforms: result.platforms?.map(p => p.name) || [], + developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], + onlineRating: result.total_rating ? Math.round(result.total_rating * 10) / 10 : 0, image: image, + released: result.first_release_date ? (result.first_release_date * 1000) <= Date.now() : false, + releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', + userData: { played: false, personalRating: 0 }, + }); + } + + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; } +} \ No newline at end of file diff --git a/api/apis/MALAPI.ts b/api/apis/MALAPI.ts new file mode 100644 index 00000000..fc0c6986 --- /dev/null +++ b/api/apis/MALAPI.ts @@ -0,0 +1,229 @@ +import createClient from 'openapi-fetch'; +import { coerceMovieDurationMinutes, coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MovieModel } from '../../models/MovieModel'; +import { SeriesModel } from '../../models/SeriesModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/MALAPI'; + +export class MALAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MALAPI'; + this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; + this.apiUrl = 'https://jikan.moe/'; + this.types = [MediaType.Movie, MediaType.Series]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('special', 'special'); + this.typeMappings.set('tv', 'series'); + this.typeMappings.set('ova', 'ova'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime', { + params: { + query: { + q: title, + limit: 20, + sfw: this.plugin.settings.sfwFilter ? true : false, + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data?.data; + + const ret: MediaTypeModel[] = []; + + for (const result of data ?? []) { + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); + const id = result.mal_id?.toString(); + + if (type === undefined) { + ret.push( + new MovieModel({ + subType: '', + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + if (type === 'movie' || type === 'special') { + ret.push( + new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } else if (type === 'series' || type === 'ova') { + ret.push( + new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + year, + dataSource: this.apiName, + id, + }), + ); + } + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/anime/{id}/full', { + params: { + path: { + id: id as unknown as number, // This is fine + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const result = response.data?.data; + + if (result === undefined) { + throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); + } + + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); + const new_id = result.mal_id?.toString(); + + if (type === undefined) { + return new MovieModel({ + subType: undefined, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: coerceMovieDurationMinutes(result.duration), + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } + + if (type === 'movie' || type === 'special') { + return new MovieModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + duration: coerceMovieDurationMinutes(result.duration), + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } else if (type === 'series' || type === 'ova') { + return new SeriesModel({ + subType: type, + title: result.title, + englishTitle: result.title_english ?? result.title, + japaneseTitle: result.title_japanese, + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + plot: result.synopsis, + genres: result.genres?.map(x => x.name).filter(isTruthy), + studio: result.studios?.map(x => x.name).filter(isTruthy), + episodes: result.episodes, + duration: result.duration, + onlineRating: result.score, + streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), + image: result.images?.jpg?.image_url, + + released: true, + ageRating: result.rating, + airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), + airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), + airing: result.airing, + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } + + throw new Error(`MDB | Unknown media type for id ${id}`); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MALAPI_disabledMediaTypes; + } +} diff --git a/api/apis/MALAPIManga.ts b/api/apis/MALAPIManga.ts new file mode 100644 index 00000000..4293ca1a --- /dev/null +++ b/api/apis/MALAPIManga.ts @@ -0,0 +1,161 @@ +import createClient from 'openapi-fetch'; +import { coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; +import type MediaDbPlugin from '../../main'; +import { ComicMangaModel } from '../../models/ComicMangaModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/MALAPI'; + +export class MALAPIManga extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MALAPI Manga'; + this.apiDescription = 'A free API for Manga. Some results may take a long time to load.'; + this.apiUrl = 'https://jikan.moe/'; + this.types = [MediaType.ComicManga]; + this.typeMappings = new Map(); + this.typeMappings.set('manga', 'manga'); + this.typeMappings.set('manhwa', 'manhwa'); + this.typeMappings.set('doujinshi', 'doujin'); + this.typeMappings.set('one-shot', 'oneshot'); + this.typeMappings.set('manhua', 'manhua'); + this.typeMappings.set('light novel', 'light-novel'); + this.typeMappings.set('novel', 'novel'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/manga', { + params: { + query: { + q: title, + limit: 20, + sfw: this.plugin.settings.sfwFilter ? true : false, + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data?.data; + + const ret: MediaTypeModel[] = []; + + for (const result of data ?? []) { + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = coerceYear(result.published?.prop?.from?.year); + const id = result.mal_id?.toString(); + + ret.push( + new ComicMangaModel({ + subType: type, + title: result.title, + plot: result.synopsis ?? undefined, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), + year: year, + dataSource: this.apiName, + url: result.url, + id: id, + + genres: result.genres?.map(x => x.name).filter(isTruthy), + authors: result.authors?.map(x => x.name).filter(isTruthy), + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), + publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); + + const response = await client.GET('/manga/{id}/full', { + params: { + path: { + id: id as unknown as number, // This is fine + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const result = response.data?.data; + + if (!result) { + throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); + } + + const resType = result.type?.toLowerCase(); + const type = resType ? this.typeMappings.get(resType) : undefined; + const year = coerceYear(result.published?.prop?.from?.year); + const new_id = result.mal_id?.toString(); + + return new ComicMangaModel({ + subType: type, + title: result.title, + plot: result.synopsis ?? undefined, + englishTitle: result.title_english ?? result.title, + alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), + year: year, + dataSource: this.apiName, + url: result.url, + id: new_id, + + genres: result.genres?.map(x => x.name).filter(isTruthy), + authors: result.authors?.map(x => x.name).filter(isTruthy), + chapters: result.chapters, + volumes: result.volumes, + onlineRating: result.score, + image: result.images?.jpg?.image_url, + + released: true, + publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), + publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), + status: result.status, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MALAPIManga_disabledMediaTypes; + } +} diff --git a/api/apis/MobyGamesAPI.ts b/api/apis/MobyGamesAPI.ts new file mode 100644 index 00000000..b201ad03 --- /dev/null +++ b/api/apis/MobyGamesAPI.ts @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ + +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +// sadly no open api schema available + +// TODO: maybe we should remove this API, as it can no longer be tested without paying for an API key + +export class MobyGamesAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-DD-MM'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MobyGamesAPI'; + this.apiDescription = 'A free API for games.'; + this.apiUrl = 'https://api.mobygames.com/v1'; + this.types = [MediaType.Game]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); + if (!apiKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${apiKey}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + // console.debug(fetchData); + + if (fetchData.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (fetchData.status === 429) { + throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); + } + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const ret: MediaTypeModel[] = []; + for (const result of data.games) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: result.title, + englishTitle: result.title, + year: new Date(result.platforms[0].first_release_date).getFullYear(), + dataSource: this.apiName, + id: result.game_id, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); + if (!apiKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${apiKey}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = await fetchData.json; + // console.debug(data); + const result = data.games[0]; + + return new GameModel({ + type: MediaType.Game, + title: result.title, + englishTitle: result.title, + year: new Date(result.platforms[0].first_release_date).getFullYear(), + dataSource: this.apiName, + url: `https://www.mobygames.com/game/${result.game_id}`, + id: result.game_id, + developers: [], + publishers: [], + genres: result.genres?.map((x: any) => x.genre_name) ?? [], + onlineRating: result.moby_score, + image: result.sample_cover?.image ?? '', + + released: true, + releaseDate: result.platforms[0].first_release_date ?? 'unknown', + + userData: { + played: false, + + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MobyGamesAPI_disabledMediaTypes; + } +} diff --git a/api/apis/MusicBrainzAPI.ts b/api/apis/MusicBrainzAPI.ts new file mode 100644 index 00000000..df6d0d16 --- /dev/null +++ b/api/apis/MusicBrainzAPI.ts @@ -0,0 +1,328 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MusicReleaseModel } from '../../models/MusicReleaseModel'; +import { MediaType } from '../../utils/MediaType'; +import { contactEmail, coerceYear, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; +import { APIModel } from '../APIModel'; + +// sadly no open api schema available + +interface Tag { + name: string; + count: number; +} +interface Genre { + name: string; + count: number; + id: string; + disambiguation: string; +} +interface Release { + id: string; + 'status-id': string; + title: string; + status: string; +} + +function pickNonBootlegRelease(releases: Release[] | undefined): Release | undefined { + return releases?.find(r => r.status !== 'Bootlet'); +} + +interface ArtistCredit { + name: string; + artist: { + tags: Tag[]; + type: string; + id: string; + name: string; + 'short-name': string; + country: string; + }; +} + +interface SearchResponse { + id: string; + 'type-id': string; + score: number; + 'primary-type-id': string; + 'artists-credit-id': string; + count: number; + title: string; + 'first-release-date': string; + 'primary-type': string; + 'artist-credit': ArtistCredit[]; + releases: Release[]; + tags: Tag[]; +} + +interface IdResponse { + id: string; + tags: Tag[]; + 'primary-type-id': string; + 'artist-credit': ArtistCredit[]; + title: string; + genres: Genre[]; + 'first-release-date': string; + releases: Release[]; + 'primary-type': string; + rating: { + value: number; + 'votes-count': number; + }; +} + +interface MediaResponse { + media: { + 'track-count': number; + tracks: { + 'artist-credit': ArtistCredit[]; + length: number | null; + number: string; + position: number; + title: string; + recording: { + id?: string; + length: number; + title: string; + }; + }[]; + }[]; + 'text-representation': { + language: string; + script: string; + }; +} + +export class MusicBrainzAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MusicBrainz API'; + this.apiDescription = 'Free API for music albums.'; + this.apiUrl = 'https://musicbrainz.org/'; + this.types = [MediaType.MusicRelease]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; + + const fetchData = await requestUrl({ + url: searchUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }); + + // console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json) as { + 'release-groups': SearchResponse[]; + }; + // console.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data['release-groups']) { + ret.push( + new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: coerceYear( + result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, + ), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + subType: result['primary-type'], + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + // Fetch release group + const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; + const groupResponse = await requestUrl({ + url: groupUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }); + + if (groupResponse.status !== 200) { + throw Error(`MDB | Received status code ${groupResponse.status} from ${this.apiName}.`); + } + + const result = (await groupResponse.json) as IdResponse; + + const firstRelease = pickNonBootlegRelease(result.releases); + if (!firstRelease) { + throw Error('MDB | No non-bootleg release found in release group.'); + } + + // Fetch recordings for the chosen release (skip MusicBrainz status=Bootleg when another edition exists) + const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; + console.log(`MDB | Fetching release recordings from: ${releaseUrl}`); + + const releaseResponse = await requestUrl({ + url: releaseUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }); + + if (releaseResponse.status !== 200) { + throw Error(`MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`); + } + + const releaseData = (await releaseResponse.json) as MediaResponse; + const tracks = extractTracksFromMedia(releaseData.media); + + // Calculate total album length for the first release + const totalrawLength = + releaseData.media[0]?.tracks.reduce((sum, track) => { + const len = track.length ?? track.recording?.length; + return typeof len === 'number' && !isNaN(len) ? sum + len : sum; + }, 0) ?? 0; + const albumLengthCalc = millisecondsToMinutes(totalrawLength); + + console.log(releaseData); + + return new MusicReleaseModel({ + type: 'musicRelease', + title: result.title, + englishTitle: result.title, + year: coerceYear( + result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, + ), + releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: 'https://musicbrainz.org/release-group/' + result.id, + id: result.id, + image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', + + artists: result['artist-credit'].map(a => a.name), + language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', + genres: result.genres.map(g => g.name), + subType: result['primary-type'], + albumDuration: albumLengthCalc, + trackCount: releaseData.media[0]?.['track-count'] ?? 0, + tracks: tracks, + rating: result.rating.value * 2, + + userData: { + personalRating: 0, + }, + }); + } + /** + * For the 'albumTitle' property (used on Song notes), the link target is + * derived from the MusicRelease file name template rather than the raw title. + */ + override wikilinkValueFor(property: string, value: string, obj: Record, folderPrefix: string): string { + if (property === 'albumTitle') { + const title = value.trim(); + const artistsRaw = obj.artists; + const artists = Array.isArray(artistsRaw) + ? artistsRaw.filter((a): a is string => typeof a === 'string') + : []; + const releaseModel = new MusicReleaseModel({ + type: 'musicRelease', title, englishTitle: title, + year: coerceYear(obj.year), releaseDate: '', dataSource: '', + url: '', id: '', image: '', artists, genres: [], + subType: 'album', language: '', rating: 0, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); + return linkTarget === title ? `[[${linkTarget}]]` : `[[${linkTarget}|${title}]]`; + } + return super.wikilinkValueFor(property, value, obj, folderPrefix); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; + } + + /** + * Loads MusicBrainz recording URL relations and returns the open.spotify.com track URL if present. + * Callers should throttle requests (~1/s) per MusicBrainz etiquette. + */ + async fetchSpotifyUrlForRecording(recordingId: string): Promise { + if (!recordingId) { + return ''; + } + const recordingUrl = `https://musicbrainz.org/ws/2/recording/${encodeURIComponent(recordingId)}?inc=url-rels&fmt=json`; + const fetchData = await requestUrl({ + url: recordingUrl, + headers: { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }, + }); + if (fetchData.status !== 200) { + console.warn(`MDB | Recording ${recordingId} url-rels returned ${fetchData.status}`); + return ''; + } + const data = (await fetchData.json) as RecordingUrlRelsResponse; + for (const rel of data.relations ?? []) { + const resource = rel.url?.resource; + if (typeof resource === 'string' && resource.includes('open.spotify.com')) { + return resource; + } + } + return ''; + } +} + +interface RecordingUrlRelsResponse { + relations?: { type: string; url?: { resource: string } }[]; +} + +function extractTracksFromMedia(media: MediaResponse['media']): { + number: number; + title: string; + duration: string; + featuredArtists: string[]; +}[] { + if (!media || media.length === 0 || !media[0].tracks) return []; + + return media[0].tracks.map((track, index) => { + const title = track.title ?? track.recording?.title ?? 'Unknown Title'; + const rawLength = track.length ?? track.recording?.length; + const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; + const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; + const recordingId = track.recording?.id; + + return { + number: index + 1, + title, + duration, + featuredArtists, + ...(recordingId ? { recordingId } : {}), + }; + }); +} + +function millisecondsToMinutes(milliseconds: number): string { + const minutes = Math.floor(milliseconds / 60000); + const seconds = Math.floor((milliseconds % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} diff --git a/api/apis/MusicBrainzArtistAPI.ts b/api/apis/MusicBrainzArtistAPI.ts new file mode 100644 index 00000000..397b6c81 --- /dev/null +++ b/api/apis/MusicBrainzArtistAPI.ts @@ -0,0 +1,249 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { ArtistModel } from '../../models/ArtistModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { coerceYear, contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; +import { APIModel } from '../APIModel'; + +interface ArtistTag { + name: string; + count: number; +} + +interface ArtistGenre { + name: string; +} + +interface ArtistSearchArtist { + id: string; + name: string; + 'life-span'?: { begin?: string; end?: string }; + country?: string; + disambiguation?: string; + isnis?: string[]; +} + +interface ArtistSearchResponse { + artists: ArtistSearchArtist[]; +} + +interface ArtistDetailResponse { + id: string; + name: string; + type?: string; + 'life-span'?: { begin?: string; end?: string }; + country?: string; + disambiguation?: string; + isnis?: string[]; + tags?: ArtistTag[]; + genres?: ArtistGenre[]; + relations?: { url?: { resource: string } | null; type: string }[]; +} + +interface ReleaseGroupListItem { + id: string; + title: string; + 'primary-type': string; + 'secondary-types'?: string[]; + 'first-release-date'?: string; +} + +interface ReleaseGroupBrowseResponse { + 'release-groups': ReleaseGroupListItem[]; +} + +function isniFromMusicBrainz(isnis: string[] | undefined): string { + if (!isnis?.length) { + return ''; + } + return isnis.join(', '); +} + +const EXCLUDED_SECONDARY_TYPES = new Set([ + 'Compilation', + 'Live', + 'Remix', + 'Soundtrack', + 'Spokenword', + 'Interview', + 'Audio drama', + 'DJ-mix', + 'Mixtape/Street', + 'Demo', + 'Field recording', +]); + +export class MusicBrainzArtistAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'MusicBrainz Artist API'; + this.apiDescription = 'MusicBrainz artist search and studio album discography.'; + this.apiUrl = 'https://musicbrainz.org/'; + this.types = [MediaType.Artist]; + } + + private mbHeaders(): Record { + return { + 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, + }; + } + + private async throttleMs(ms: number): Promise { + await new Promise(resolve => setTimeout(resolve, ms)); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://musicbrainz.org/ws/2/artist?query=${encodeURIComponent(title)}&limit=20&fmt=json`; + const fetchData = await requestUrl({ + url: searchUrl, + headers: this.mbHeaders(), + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json) as ArtistSearchResponse; + const ret: MediaTypeModel[] = []; + + for (const artist of data.artists ?? []) { + const begin = artist['life-span']?.begin; + ret.push( + new ArtistModel({ + type: 'artist', + title: artist.name, + englishTitle: artist.name, + year: coerceYear(begin ? (begin.split('-')[0] ?? '') : ''), + beginYear: begin ? (begin.split('-')[0] ?? '') : '', + releaseDate: '', + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: 'https://musicbrainz.org/artist/' + artist.id, + id: artist.id, + country: artist.country ?? '', + disambiguation: artist.disambiguation ?? '', + isni: isniFromMusicBrainz(artist.isnis), + genres: [], + image: '', + officialWebsite: '', + subType: 'artist', + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const artistUrl = `https://musicbrainz.org/ws/2/artist/${encodeURIComponent(id)}?inc=tags+genres+url-rels&fmt=json`; + const res = await requestUrl({ + url: artistUrl, + headers: this.mbHeaders(), + }); + + if (res.status !== 200) { + throw Error(`MDB | Received status code ${res.status} from ${this.apiName}.`); + } + + const artist = (await res.json) as ArtistDetailResponse; + const begin = artist['life-span']?.begin; + const beginYear = begin ? (begin.split('-')[0] ?? '') : ''; + + let officialWebsite = ''; + for (const rel of artist.relations ?? []) { + if (rel.type === 'official homepage' && rel.url?.resource) { + officialWebsite = rel.url.resource; + break; + } + } + + return new ArtistModel({ + type: 'artist', + title: artist.name, + englishTitle: artist.name, + year: coerceYear(beginYear), + beginYear, + releaseDate: begin ? (this.plugin.dateFormatter.format(begin, this.apiDateFormat) ?? 'unknown') : '', + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: 'https://musicbrainz.org/artist/' + artist.id, + id: artist.id, + country: artist.country ?? '', + disambiguation: artist.disambiguation ?? '', + isni: isniFromMusicBrainz(artist.isnis), + genres: [...new Set([...(artist.genres?.map(g => g.name) ?? []), ...(artist.tags?.map(t => t.name) ?? [])])], + image: '', + officialWebsite, + subType: 'artist', + userData: { + personalRating: 0, + }, + }); + } + + /** + * Lists release group MBIDs for studio albums (primary type album, excluding live/compilations/etc.). + * Passes release-group-status=website-default so MusicBrainz omits groups that only have bootleg, promotional, or pseudo-releases + * (see MusicBrainz API “Release (Group) Type and Status”). + */ + async listStudioAlbumReleaseGroupIds(artistId: string): Promise { + const collected: { id: string; date: string }[] = []; + let offset = 0; + const limit = 100; + + while (true) { + await this.throttleMs(1100); + const url = `https://musicbrainz.org/ws/2/release-group?artist=${encodeURIComponent(artistId)}&type=album&fmt=json&limit=${limit}&offset=${offset}&release-group-status=website-default`; + + const res = await requestUrl({ + url, + headers: this.mbHeaders(), + }); + + if (res.status !== 200) { + throw Error(`MDB | Received status code ${res.status} browsing release groups.`); + } + + const data = (await res.json) as ReleaseGroupBrowseResponse; + const groups = data['release-groups'] ?? []; + if (groups.length === 0) { + break; + } + + for (const rg of groups) { + if (rg['primary-type'] !== 'Album') { + continue; + } + const secondary = rg['secondary-types'] ?? []; + if (secondary.some(t => EXCLUDED_SECONDARY_TYPES.has(t))) { + continue; + } + collected.push({ + id: rg.id, + date: rg['first-release-date'] ?? '', + }); + } + + offset += limit; + if (groups.length < limit) { + break; + } + } + + collected.sort((a, b) => a.date.localeCompare(b.date)); + return [...new Set(collected.map(c => c.id))]; + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.MusicBrainzArtistAPI_disabledMediaTypes; + } +} diff --git a/api/apis/OMDbAPI.ts b/api/apis/OMDbAPI.ts new file mode 100644 index 00000000..28d69242 --- /dev/null +++ b/api/apis/OMDbAPI.ts @@ -0,0 +1,291 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MovieModel } from '../../models/MovieModel'; +import { SeriesModel } from '../../models/SeriesModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { MediaType } from '../../utils/MediaType'; +import { coerceMovieDurationMinutes, coerceYear } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +interface ErrorResponse { + Response: 'False'; + Error: string; +} + +type SearchResponse = + | { + Response: 'True'; + totalResults: string; + Search: { + Title: string; + Year: string; + Poster: string; + imdbID: string; + Type: string; + }[]; + } + | ErrorResponse; + +type IdResponse = + | { + Response: 'True'; + Title: string; + Year: string; + Rated: string; + Released: string; + Runtime: string; + Genre: string; + Director: string; + Writer: string; + Actors: string; + Plot: string; + Language: string; + Country: string; + Awards: string; + Poster: string; + Metascore: string; + imdbRating: string; + imdbVotes: string; + imdbID: string; + Type: string; + DVD: string; + BoxOffice: string; + Production: string; + Website: string; + } + | ErrorResponse; + +export class OMDbAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'DD MMM YYYY'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OMDbAPI'; + this.apiDescription = 'A free API for Movies, Series and Games.'; + this.apiUrl = 'https://www.omdbapi.com/'; + this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; + this.typeMappings = new Map(); + this.typeMappings.set('movie', 'movie'); + this.typeMappings.set('series', 'series'); + this.typeMappings.set('game', 'game'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); + if (!omdbKey) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const response = await requestUrl({ + url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${omdbKey}`, + method: 'GET', + }); + + if (response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.status !== 200) { + throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); + } + + const data = response.json as SearchResponse | undefined; + + if (!data) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + + if (data.Response === 'False') { + if (data.Error === 'Movie not found!') { + return []; + } + + throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`); + } + if (!data.Search) { + return []; + } + + // console.debug(data.Search); + + const ret: MediaTypeModel[] = []; + + for (const result of data.Search) { + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + continue; + } + if (type === 'movie') { + ret.push( + new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'series') { + ret.push( + new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } else if (type === 'game') { + ret.push( + new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + id: result.imdbID, + }), + ); + } + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); + if (!omdbKey) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const response = await requestUrl({ + url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${omdbKey}`, + method: 'GET', + }); + + if (response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.status !== 200) { + throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); + } + + const result = response.json as IdResponse | undefined; + + if (!result) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + + if (result.Response === 'False') { + throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); + } + + const type = this.typeMappings.get(result.Type.toLowerCase()); + if (type === undefined) { + throw Error(`${result.Type.toLowerCase()} is an unsupported type.`); + } + + if (type === 'movie') { + return new MovieModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + director: result.Director?.split(', '), + writer: result.Writer?.split(', '), + duration: coerceMovieDurationMinutes(result.Runtime), + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + revenue: result.BoxOffice && result.BoxOffice !== 'N/A' ? result.BoxOffice : '', + ageRating: result.Rated, + premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } else if (type === 'series') { + return new SeriesModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + plot: result.Plot, + genres: result.Genre?.split(', '), + writer: result.Writer?.split(', '), + studio: [], + episodes: 0, + duration: result.Runtime, + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', '), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + country: result.Country?.split(', '), + ageRating: result.Rated, + airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } else if (type === 'game') { + return new GameModel({ + type: type, + title: result.Title, + englishTitle: result.Title, + year: coerceYear(result.Year), + dataSource: this.apiName, + url: `https://www.imdb.com/title/${result.imdbID}/`, + id: result.imdbID, + + genres: result.Genre?.split(', '), + onlineRating: Number.parseFloat(result.imdbRating ?? 0), + image: result.Poster.replace('_SX300', '_SX600'), + + released: true, + releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + }); + } + + throw new Error(`MDB | Unknown media type for id ${id}`); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OMDbAPI_disabledMediaTypes; + } +} diff --git a/api/apis/OpenLibraryAPI.ts b/api/apis/OpenLibraryAPI.ts new file mode 100644 index 00000000..ae9b74ed --- /dev/null +++ b/api/apis/OpenLibraryAPI.ts @@ -0,0 +1,161 @@ +import createClient from 'openapi-fetch'; +import { BookModel } from 'src/models/BookModel'; +import { obsidianFetch } from 'src/utils/Utils'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/OpenLibrary'; + +interface SearchResponse { + editions: { + docs: { + key?: string; + title?: string; + cover_i?: number; + isbn?: string[]; + }[]; + }; + cover_i?: number; + has_fulltext?: boolean; + edition_count?: number; + title?: string; + author_name?: string[]; + first_publish_year?: number; + key: string; + description?: string; + + number_of_pages_median?: number; + isbn?: string[]; + ratings_average?: number; +} + +export class OpenLibraryAPI extends APIModel { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'OpenLibraryAPI'; + this.apiDescription = 'A free API for books'; + this.apiUrl = 'https://openlibrary.org/'; + this.types = [MediaType.Book]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const response = await client.GET('/search.json', { + params: { + query: { + q: title, + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data as { + docs: SearchResponse[]; + }; + + // console.debug(data); + + const ret: MediaTypeModel[] = []; + + for (const result of data.docs) { + ret.push( + new BookModel({ + title: result.title, + englishTitle: result.title, + year: result.first_publish_year ?? 0, + dataSource: this.apiName, + id: result.key, + author: result.author_name?.join(', '), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + + const response = await client.GET('/search.json', { + params: { + query: { + q: `${id}`, + fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', + }, + }, + fetch: obsidianFetch, + }); + + if (response.error !== undefined) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data as { + docs: SearchResponse[]; + q?: string; + }; + + const result = data.docs[0]; + + let key = result.key; + let title = result.title; + let cover_i = result.cover_i; + let isbnArr = result.isbn; + + // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present + const q = data.q ?? ''; + if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { + const edition = result.editions.docs[0]; + key = edition.key ?? key; + title = edition.title ?? title; + cover_i = edition.cover_i ?? cover_i; + isbnArr = edition.isbn ?? isbnArr; + } + + const pages = Number(result.number_of_pages_median); + const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); + const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); + + return new BookModel({ + title: title, + year: result.first_publish_year ?? 0, + dataSource: this.apiName, + url: `https://openlibrary.org` + key, + id: key, + isbn: Number.isNaN(isbn) ? undefined : isbn, + isbn13: Number.isNaN(isbn13) ? undefined : isbn13, + englishTitle: title, + + author: result.author_name?.join(', '), + plot: result.description ?? undefined, + pages: Number.isNaN(pages) ? undefined : pages, + onlineRating: result.ratings_average, + image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, + + released: true, + + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; + } +} diff --git a/api/apis/RAWGAPI.ts b/api/apis/RAWGAPI.ts new file mode 100644 index 00000000..c22c9a9a --- /dev/null +++ b/api/apis/RAWGAPI.ts @@ -0,0 +1,68 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +interface RAWGGame { + id: number; name: string; released?: string; background_image?: string; + name_original?: string; website?: string; slug?: string; metacritic?: number; + developers?: { name: string }[]; publishers?: { name: string }[]; genres?: { name: string }[]; +} +interface RAWGSearchResponse { results: RAWGGame[]; } + +export class RAWGAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; + + constructor(plugin: MediaDbPlugin) { + super(); + this.plugin = plugin; + this.apiName = 'RAWGAPI'; + this.apiDescription = 'A large open video game database.'; + this.apiUrl = 'https://api.rawg.io/api'; + this.types = [MediaType.Game]; + } + + async searchByTitle(title: string): Promise { + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); + if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const response = await requestUrl({ + url: `${this.apiUrl}/games?key=${apiKey}&search=${encodeURIComponent(title)}&page_size=20`, + method: 'GET', + }); + if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + + const data = response.json as RAWGSearchResponse; + return data.results.map(result => new GameModel({ + type: MediaType.Game, title: result.name, englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear() : 0, + dataSource: this.apiName, id: result.id.toString(), image: result.background_image + })); + } + + async getById(id: string): Promise { + const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); + if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); + const response = await requestUrl({ + url: `${this.apiUrl}/games/${id}?key=${apiKey}`, + method: 'GET', + }); + if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); + + const result = response.json as RAWGGame; + return new GameModel({ + type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, + year: result.released ? new Date(result.released).getFullYear() : 0, + dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], + publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], + onlineRating: result.metacritic, image: result.background_image, + released: result.released != null, releaseDate: result.released, + userData: { played: false, personalRating: 0 }, + }); + } + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; } +} \ No newline at end of file diff --git a/api/apis/SteamAPI.ts b/api/apis/SteamAPI.ts new file mode 100644 index 00000000..8d8f78a0 --- /dev/null +++ b/api/apis/SteamAPI.ts @@ -0,0 +1,245 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { coerceYear, imageUrlExists } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +interface SearchResponse { + appid: string; + name: string; + icon: string; + logo: string; +} + +type IdResponse = Record< + string, + { + success: boolean; + data: GameDetails; + } +>; + +interface GameDetails { + type: string; + name: string; + steam_appid: number; + required_age: string; + is_free: boolean; + controller_support: string; + dlc: number[]; + detailed_description: string; + about_the_game: string; + short_description: string; + supported_languages: string; + reviews: string; + header_image: string; + capsule_image: string; + capsule_imagev5: string; + website: string; + pc_requirements: Requirements; + mac_requirements: Requirements; + linux_requirements: Requirements; + legal_notice: string; + drm_notice: string; + developers: string[]; + publishers: string[]; + price_overview: PriceOverview; + packages: number[]; + platforms: Platforms; + metacritic?: { + score: number; + url: string; + }; + categories: Category[]; + genres: Genre[]; + recommendations: { + total: number; + }; + achievements: { + total: number; + highlighted: Achievement[]; + }; + release_date: { + coming_soon: boolean; + date: string; + }; + support_info: { + url: string; + email: string; + }; + background: string; + background_raw: string; + content_descriptors: { + ids: number[]; + notes: string; + }; + ratings: Ratings; +} + +interface Requirements { + minimum: string; + recommended: string; +} + +interface PriceOverview { + currency: string; + initial: number; + final: number; + discount_percent: number; + initial_formatted: string; + final_formatted: string; +} + +interface Platforms { + windows: boolean; + mac: boolean; + linux: boolean; +} + +interface Category { + id: number; + description: string; +} + +interface Genre { + id: string; + description: string; +} + +interface Achievement { + name: string; + path: string; +} + +type Ratings = Record< + string, + { + rating: string; + descriptors: string; + use_age_gate: string; + required_age: string; + rating_id?: string; + banned?: string; + rating_generated?: string; + } +>; + +export class SteamAPI extends APIModel { + plugin: MediaDbPlugin; + typeMappings: Map; + apiDateFormat: string = 'DD MMM, YYYY'; + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'SteamAPI'; + this.apiDescription = 'A free API for all Steam games.'; + this.apiUrl = 'https://www.steampowered.com/'; + this.types = [MediaType.Game]; + this.typeMappings = new Map(); + this.typeMappings.set('game', 'game'); + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json) as SearchResponse[]; + + // console.debug(data); + + const ret: MediaTypeModel[] = []; + + for (const result of data) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: 0, + dataSource: this.apiName, + id: result.appid, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; + const fetchData = await requestUrl({ + url: searchUrl, + }); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + // console.debug(await fetchData.json); + const data = (await fetchData.json) as IdResponse; + + let result: GameDetails | undefined = undefined; + for (const [key, value] of Object.entries(data)) { + // after some testing I found out that id is somehow a number despite that it's defined as string... + if (key === String(id)) { + result = value.data; + } + } + if (!result) { + throw Error(`MDB | API returned invalid data.`); + } + + // console.debug(result); + + // Check if a poster version of the image exists, else use the header image + const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; + const exists = await imageUrlExists(imageUrl); + let finalimageurl; + if (exists) { + finalimageurl = imageUrl; + } else { + finalimageurl = result.header_image ?? ''; + } + + return new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: coerceYear(new Date(result.release_date.date).getFullYear()), + dataSource: this.apiName, + url: `https://store.steampowered.com/app/${result.steam_appid}`, + id: result.steam_appid.toString(), + + developers: result.developers, + publishers: result.publishers, + genres: result.genres?.map(x => x.description), + onlineRating: result.metacritic?.score, + image: finalimageurl, + + released: !result.release_date?.coming_soon, + releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), + + userData: { + played: false, + personalRating: 0, + }, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.SteamAPI_disabledMediaTypes; + } +} diff --git a/api/apis/TMDBMovieAPI.ts b/api/apis/TMDBMovieAPI.ts new file mode 100644 index 00000000..ea0cd19a --- /dev/null +++ b/api/apis/TMDBMovieAPI.ts @@ -0,0 +1,202 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ + +import createClient from 'openapi-fetch'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MovieModel } from '../../models/MovieModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { MediaType } from '../../utils/MediaType'; +import { formatUsdWholeDollars } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/TMDB'; + +/** TMDB `credits.crew` jobs that count as writing credits for movies. */ +const TMDB_WRITING_CREW_JOBS = new Set([ + 'Writer', + 'Screenplay', + 'Story', + 'Teleplay', + 'Original Story', + 'Characters', + 'Novel', + 'Screenstory', +]); + +function tmdbWritingCreditsFromCrew(crew: any[] | undefined): string[] { + if (!crew?.length) { + return []; + } + const seen = new Set(); + const names: string[] = []; + for (const c of crew) { + const job = c?.job as string | undefined; + const name = c?.name as string | undefined; + if (!job || !name || !TMDB_WRITING_CREW_JOBS.has(job)) { + continue; + } + if (!seen.has(name)) { + seen.add(name); + names.push(name); + } + } + return names; +} + +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`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const response = await client.GET('/3/search/movie', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: fetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data; + + if (!data) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + + if (data.total_results === 0 || !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() : 0, + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const response = await client.GET('/3/movie/{movie_id}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { movie_id: parseInt(id) }, + query: { + append_to_response: 'credits,release_dates,watch/providers', + }, + }, + fetch: fetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const result = response.data; + + if (!result) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + // console.debug(result); + + return new MovieModel({ + type: 'movie', + title: result.title, + englishTitle: result.title, + year: result.release_date ? new Date(result.release_date).getFullYear() : 0, + 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.toString(), + + plot: result.overview ?? '', + genres: result.genres?.map((g: any) => g.name) ?? [], + // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type + writer: tmdbWritingCreditsFromCrew((result as { credits?: { crew?: any[] } }).credits?.crew), + // @ts-ignore + 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 != null && Number.isFinite(result.runtime) ? Math.trunc(result.runtime) : 0, + onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, + // @ts-ignore + 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!), + country: result.production_countries?.map((c: any) => c.name) ?? [], + language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], + budget: formatUsdWholeDollars(result.budget ?? 0), + revenue: formatUsdWholeDollars(result.revenue ?? 0), + // @ts-ignore + ageRating: result.release_dates?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.release_dates?.[0]?.certification ?? '', + // @ts-ignore + streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], + + userData: { + watched: false, + lastWatched: '', + personalRating: 0, + }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; + } +} diff --git a/api/apis/TMDBSeasonAPI.ts b/api/apis/TMDBSeasonAPI.ts new file mode 100644 index 00000000..b91b50a0 --- /dev/null +++ b/api/apis/TMDBSeasonAPI.ts @@ -0,0 +1,277 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ + +import createClient from 'openapi-fetch'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { SeasonModel } from '../../models/SeasonModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/TMDB'; + +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`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const searchResponse = await client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: fetch, + }); + + if (searchResponse.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + + if (searchResponse.response.status !== 200) { + throw Error(`MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`); + } + + const searchData = searchResponse.data; + + if (!searchData?.results || searchData.total_results === 0) { + return []; + } + + const ret: MediaTypeModel[] = []; + + for (const result of searchData.results) { + if (ret.length >= 20) break; + + // Fetch series details to get the total number of seasons + let totalSeasons = 0; + try { + const detailsResponse = await client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { series_id: result.id ?? 0 }, + }, + fetch: fetch, + }); + + if (detailsResponse.response.status === 200 && detailsResponse.data) { + const detailsData = detailsResponse.data; + if (Array.isArray(detailsData.seasons)) { + totalSeasons = detailsData.seasons.length; + } + } + } catch { + // Ignore errors and assume 0 seasons + } + + ret.push( + new SeasonModel({ + title: `${result.name ?? result.original_name ?? ''}`, + englishTitle: result.name ?? result.original_name ?? '', + year: result.first_air_date ? new Date(result.first_air_date).getFullYear() : 0, + dataSource: this.apiName, + id: result.id?.toString() ?? '', + seasonTitle: result.name ?? result.original_name ?? '', + seasonNumber: totalSeasons, + }), + ); + } + + return ret; + } + + // Fetch all seasons for a given series + async getSeasonsForSeries(tvId: string): Promise { + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const seriesResponse = await client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { series_id: parseInt(tvId) }, + }, + fetch: fetch, + }); + + if (seriesResponse.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + + if (seriesResponse.response.status !== 200) { + throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); + } + + const seriesData = seriesResponse.data; + const seriesName = seriesData?.name ?? ''; + + const ret: SeasonModel[] = []; + + if (Array.isArray(seriesData?.seasons)) { + for (const season of seriesData.seasons) { + const seasonNumber = season.season_number ?? 0; + const titleText = `${seriesName} - Season ${seasonNumber}`; + + ret.push( + new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: season.air_date ? new Date(season.air_date).getFullYear() : 0, + dataSource: this.apiName, + id: `${tvId}/season/${seasonNumber}`, + seasonTitle: season.name ?? titleText, + seasonNumber: seasonNumber, + }), + ); + } + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + // Expect season ids like "12345/season/2" + const m = /^(\d+)\/season\/(\d+)$/.exec(id); + if (!m) { + throw Error(`MDB | Invalid season id "${id}". Expected format "/season/".`); + } + + const tvId = m[1]; + const seasonNumber = m[2]; + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + + // Fetch season details + const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { + series_id: parseInt(tvId), + season_number: parseInt(seasonNumber), + }, + }, + fetch: fetch, + }); + + if (seasonResponse.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + + if (seasonResponse.response.status !== 200) { + throw Error(`MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`); + } + + const seasonData = seasonResponse.data; + if (!seasonData) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + // Fetch parent series to build consistent titles and inherit fields + const seriesResponse = await client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { series_id: parseInt(tvId) }, + query: { + append_to_response: 'credits', + }, + }, + fetch: fetch, + }); + + if (seriesResponse.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + + if (seriesResponse.response.status !== 200) { + throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); + } + + const seriesData = seriesResponse.data; + + if (!seriesData) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + + const seriesName = seriesData?.name ?? ''; + const airDate = seasonData.air_date ?? ''; + const titleText = `${seriesName} - Season ${seasonData.season_number}`; + + // Get airedTo as the air_date of the last episode, if available + let airedTo = 'unknown'; + if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { + const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; + if (lastEp?.air_date) airedTo = lastEp.air_date; + } + + return new SeasonModel({ + title: titleText, + englishTitle: titleText, + year: airDate ? new Date(airDate).getFullYear() : 0, + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`, + id: `${tvId}/season/${seasonData.season_number}`, + seasonTitle: seasonData.name ?? titleText, + seasonNumber: seasonData.season_number ?? Number(seasonNumber), + episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, + airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', + airedTo: airedTo, + plot: seasonData.overview ?? '', + image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', + genres: seriesData.genres?.map(g => g.name ?? '').filter(name => name !== '') ?? [], + writer: seriesData.created_by?.map(c => c.name ?? '').filter(name => name !== '') ?? [], + studio: seriesData.production_companies?.map(s => s.name ?? '').filter(name => name !== '') ?? [], + duration: seriesData.episode_run_time?.[0]?.toString() ?? '', + onlineRating: seasonData.vote_average ?? 0, + // @ts-ignore - append_to_response credits not reflected in base schema + actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [], + released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), + streamingServices: [], + airing: ['Returning Series'].includes(seriesData.status ?? ''), + userData: { watched: false, lastWatched: '', personalRating: 0 }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return []; + } +} diff --git a/api/apis/TMDBSeriesAPI.ts b/api/apis/TMDBSeriesAPI.ts new file mode 100644 index 00000000..61fe68f3 --- /dev/null +++ b/api/apis/TMDBSeriesAPI.ts @@ -0,0 +1,168 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ + +import createClient from 'openapi-fetch'; +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; +import { SeriesModel } from '../../models/SeriesModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; +import type { paths } from '../schemas/TMDB'; + +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`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw new Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const response = await client.GET('/3/search/tv', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + query: { + query: encodeURIComponent(title), + include_adult: this.plugin.settings.sfwFilter ? false : true, + }, + }, + fetch: fetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const data = response.data; + + if (!data) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + + if (data.total_results === 0 || !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() : 0, + dataSource: this.apiName, + id: result.id.toString(), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); + if (!bearer) { + throw Error(`MDB | API key for ${this.apiName} missing.`); + } + + const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); + const response = await client.GET('/3/tv/{series_id}', { + headers: { + Authorization: `Bearer ${bearer}`, + }, + params: { + path: { series_id: parseInt(id) }, + query: { + append_to_response: 'credits,content_ratings,watch/providers', + }, + }, + fetch: fetch, + }); + + if (response.response.status === 401) { + throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); + } + if (response.response.status !== 200) { + throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); + } + + const result = response.data; + + if (!result) { + throw Error(`MDB | No data received from ${this.apiName}.`); + } + // 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() : 0, + dataSource: this.apiName, + url: `https://www.themoviedb.org/tv/${result.id}`, + id: result.id.toString(), + + 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]?.toString() ?? 'unknown', + onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, + // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type + // @ts-ignore + actors: result.credits?.cast.map((c: any) => c.name).slice(0, 5) ?? [], + image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null, + + released: ['Returning Series', 'Cancelled', 'Canceled', 'Pilot', 'Ended'].includes(result.status!), + country: result.production_countries?.map((c: any) => c.name) ?? [], + language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], + network: result.networks?.map((n: any) => n.name) ?? [], + // @ts-ignore + ageRating: result.content_ratings?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.rating ?? '', + // @ts-ignore + streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], + 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; + } +} diff --git a/api/apis/VNDBAPI.ts b/api/apis/VNDBAPI.ts new file mode 100644 index 00000000..2a933dae --- /dev/null +++ b/api/apis/VNDBAPI.ts @@ -0,0 +1,267 @@ +import { requestUrl } from 'obsidian'; +import type MediaDbPlugin from '../../main'; +import { GameModel } from '../../models/GameModel'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; +import { APIModel } from '../APIModel'; + +enum VNDevStatus { + Finished, + InDevelopment, + Cancelled, +} + +enum TagSpoiler { + None, + Minor, + Major, +} + +enum TagCategory { + Content = 'cont', + Sexual = 'ero', + Technical = 'tech', +} + +/** + * A partial `POST /vn` response payload; desired fields should be listed in the request body. + */ +interface VNJSONResponse { + more: boolean; + results: [ + { + id: string; + title: string; + titles: [ + { + title: string; + lang: string; + }, + ]; + devstatus: VNDevStatus; + released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents + image: { + url: string; + sexual: number; + } | null; + rating: number | null; + tags: [ + { + id: string; + name: string; + category: TagCategory; + rating: number; + spoiler: TagSpoiler; + }, + ]; + developers: [ + { + id: string; + name: string; + }, + ]; + }, + ]; +} + +/** + * A partial `POST /release` response payload; desired fields should be listed in the request body. + */ +interface ReleaseJSONResponse { + more: boolean; + results: [ + { + id: string; + producers: [ + { + id: string; + name: string; + developer: boolean; + publisher: boolean; + }, + ]; + }, + ]; +} + +export class VNDBAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'VNDB API'; + this.apiDescription = 'A free API for visual novels.'; + this.apiUrl = 'https://api.vndb.org/kana'; + this.types = [MediaType.Game]; + } + + /** + * Make a `POST` request to the VNDB API. + * @param endpoint The API endpoint to query. E.g. "/vn". + * @param body A JSON object defining the query, following the VNDB API structure. + * @returns A JSON object representing the query response. + * @throws Error The request returned an unsuccessful or unexpected HTTP status code. + * @see {@link https://api.vndb.org/kana#api-structure} + */ + private async postQuery(endpoint: string, body: string): Promise { + const fetchData = await requestUrl({ + url: `${this.apiUrl}${endpoint}`, + method: 'POST', + contentType: 'application/json', + body: body, + throw: false, + }); + + if (fetchData.status !== 200) { + switch (fetchData.status) { + case 400: + throw Error(`MDB | Invalid request body or query [${fetchData.text}].`); + case 404: + throw Error(`MDB | Invalid API path or HTTP method.`); + case 429: + throw Error(`MDB | Throttled.`); + case 500: + throw Error(`MDB | VNDB server error.`); + case 502: + throw Error(`MDB | VNDB server is down.`); + default: + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + } + + return fetchData.json; + } + + /** + * Make a `POST` request to the `/vn` endpoint. + * Queries visual novel entries. + * @see {@link https://api.vndb.org/kana#post-vn} + */ + private postVNQuery(body: string): Promise { + return this.postQuery('/vn', body) as Promise; + } + + /** + * Make a `POST` request to the `/release` endpoint. + * Queries release entries. + * @see {@link https://api.vndb.org/kana#post-release} + */ + private postReleaseQuery(body: string): Promise { + return this.postQuery('/release', body) as Promise; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + /* SFW Filter: has ANY official&&complete&&standalone&&SFW release + OR has NO official&&standalone&&NSFW release + OR has the `In-game Sexual Content Toggle` (g2708) tag */ + // prettier-ignore + const vnData = await this.postVNQuery(`{ + "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : + `, ["or" + , ["release", "=", ["and" + , ["official", "=", "1"] + , ["rtype", "=", "complete"] + , ["patch", "!=", "1"] + , ["has_ero", "!=", "1"] + ]] + , ["release", "!=", ["and" + , ["official", "=", "1"] + , ["patch", "!=", "1"] + , ["has_ero", "=", "1"] + ]] + , ["tag", "=", "g2708"] + ]`} + , ["search", "=", "${title}"] + ], + "fields": "title, titles{title, lang}, released", + "sort": "searchrank", + "results": 20 + }`); + + const ret: MediaTypeModel[] = []; + for (const vn of vnData.results) { + ret.push( + new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: coerceYear( + vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear() : 0, + ), + dataSource: this.apiName, + id: vn.id, + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const vnData = await this.postVNQuery(`{ + "filters": ["id", "=", "${id}"], + "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" + }`); + + if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); + const vn = vnData.results[0]; + const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; + vn.released ??= 'Unknown'; + + const releaseData = await this.postReleaseQuery(`{ + "filters": ["and" + , ["vn", "=" + , ["id", "=", "${id}"] + ] + , ["official", "=", 1] + ], + "fields": "producers.name, producers.publisher, producers.developer", + "results": 100 + }`); + + return new GameModel({ + type: MediaType.Game, + title: vn.title, + englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, + year: coerceYear(releasedIsDate ? new Date(vn.released).getFullYear() : vn.released), + dataSource: this.apiName, + url: `https://vndb.org/${vn.id}`, + id: vn.id, + + developers: vn.developers.map(d => d.name), + publishers: releaseData.results + .flatMap(r => r.producers) + .filter(p => p.publisher) + .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list + .map(p => p.name) + .unique(), + genres: vn.tags + .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) + .sort((t1, t2) => t2.rating - t1.rating) + .map(t => t.name), + onlineRating: vn.rating ?? NaN, + // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively + image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, + + released: vn.devstatus === VNDevStatus.Finished, + releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, + + userData: { + played: false, + personalRating: 0, + }, + }); + } + + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.VNDBAPI_disabledMediaTypes; + } +} diff --git a/api/apis/WikipediaAPI.ts b/api/apis/WikipediaAPI.ts new file mode 100644 index 00000000..d4a62bcc --- /dev/null +++ b/api/apis/WikipediaAPI.ts @@ -0,0 +1,112 @@ +import type MediaDbPlugin from '../../main'; +import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { WikiModel } from '../../models/WikiModel'; +import { MediaType } from '../../utils/MediaType'; +import { APIModel } from '../APIModel'; + +interface SearchResponse { + query: { + search: { + title: string; + pageid: number; + }[]; + }; +} + +interface IdResponse { + query: { + pages: Record; + }; +} + +interface WikipediaPage { + pageid: number; + title: string; + contentmodel: string; + pagelanguage: string; + pagelanguagehtmlcode: string; + pagelanguagedir: string; + touched: string; // ISO date string + lastrevid: number; + length: number; + fullurl: string; + editurl: string; + canonicalurl: string; +} +export class WikipediaAPI extends APIModel { + plugin: MediaDbPlugin; + apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO + + constructor(plugin: MediaDbPlugin) { + super(); + + this.plugin = plugin; + this.apiName = 'Wikipedia API'; + this.apiDescription = 'The API behind Wikipedia'; + this.apiUrl = 'https://www.wikipedia.com'; + this.types = [MediaType.Wiki]; + } + + async searchByTitle(title: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by Title`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; + const fetchData = await fetch(searchUrl); + // console.debug(fetchData); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json()) as SearchResponse; + console.debug(data); + const ret: MediaTypeModel[] = []; + + for (const result of data.query.search) { + ret.push( + new WikiModel({ + type: 'wiki', + title: result.title, + englishTitle: result.title, + year: 0, + dataSource: this.apiName, + id: result.pageid.toString(), + }), + ); + } + + return ret; + } + + async getById(id: string): Promise { + console.log(`MDB | api "${this.apiName}" queried by ID`); + + const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; + const fetchData = await fetch(searchUrl); + + if (fetchData.status !== 200) { + throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); + } + + const data = (await fetchData.json()) as IdResponse; + // console.debug(data); + const result = Object.values(data?.query?.pages)[0]; + + return new WikiModel({ + title: result.title, + englishTitle: result.title, + dataSource: this.apiName, + url: result.fullurl, + id: result.pageid.toString(), + + wikiUrl: result.fullurl, + lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), + length: result.length, + + userData: {}, + }); + } + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.WikipediaAPI_disabledMediaTypes; + } +} diff --git a/api/geniusLyricsExtract.ts b/api/geniusLyricsExtract.ts new file mode 100644 index 00000000..000e9218 --- /dev/null +++ b/api/geniusLyricsExtract.ts @@ -0,0 +1,87 @@ +const LYRICS_CONTAINER_OPEN_RE = + /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; + +function stripHtmlToPlainLyrics(fragment: string): string { + return fragment + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/\n{3,}/g, '\n\n') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); +} + +/** Parses nested
blocks; the naive `*?
` regex stops at the first inner close tag. */ +function extractBalancedDivInnerHtml(html: string, contentStart: number): string { + let depth = 1; + let i = contentStart; + const openRe = //gi; + while (depth > 0) { + openRe.lastIndex = i; + closeRe.lastIndex = i; + const om = openRe.exec(html); + const cm = closeRe.exec(html); + if (!cm) { + break; + } + const oIdx = om ? om.index : Number.POSITIVE_INFINITY; + const cIdx = cm.index; + if (om && oIdx < cIdx) { + depth++; + i = om.index + om[0].length; + } else { + depth--; + if (depth === 0) { + return html.slice(contentStart, cIdx); + } + i = cm.index + cm[0].length; + } + } + return ''; +} + +function collectLyricsContainersRegex(html: string): string[] { + const chunks: string[] = []; + let m: RegExpExecArray | null; + LYRICS_CONTAINER_OPEN_RE.lastIndex = 0; + while ((m = LYRICS_CONTAINER_OPEN_RE.exec(html)) !== null) { + const inner = extractBalancedDivInnerHtml(html, m.index + m[0].length); + if (inner) { + chunks.push(inner); + } + } + return chunks; +} + +function extractOneContainerPlain(el: Element): string { + const clone = el.cloneNode(true) as Element; + clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(node => node.remove()); + return stripHtmlToPlainLyrics(clone.innerHTML); +} + +export function extractLyricsFromGeniusHtml(html: string): string { + let chunks: string[] = []; + try { + const doc = new DOMParser().parseFromString(html, 'text/html'); + doc.querySelectorAll('[data-lyrics-container="true"]').forEach(c => { + const plain = extractOneContainerPlain(c); + if (plain) { + chunks.push(plain); + } + }); + } catch { + chunks = []; + } + + if (chunks.length === 0) { + return '' + } + + return chunks.join('\n\n').replace(/\n{3,}/g, '\n\n').trim(); +} diff --git a/api/musicBrainzConstants.ts b/api/musicBrainzConstants.ts new file mode 100644 index 00000000..bb218884 --- /dev/null +++ b/api/musicBrainzConstants.ts @@ -0,0 +1,19 @@ +import { MediaType } from '../utils/MediaType'; + +/** Stored on notes for any row backed by MusicBrainz (release, artist, or song). */ +export const MUSICBRAINZ_NOTE_DATA_SOURCE = 'MusicBrainz'; + +export function isMusicBrainzFamilyDataSource(dataSource: string): boolean { + return dataSource.contains('MusicBrainz'); +} + +/** Which registered API implements getById for this media type. */ +export function musicBrainzRegisteredApiName(mediaType: MediaType): 'MusicBrainz API' | 'MusicBrainz Artist API' | undefined { + if (mediaType === MediaType.Artist) { + return 'MusicBrainz Artist API'; + } + if (mediaType === MediaType.MusicRelease || mediaType === MediaType.Song) { + return 'MusicBrainz API'; + } + return undefined; +} diff --git a/api/schemas/GiantBomb.json b/api/schemas/GiantBomb.json new file mode 100644 index 00000000..4b8a13c3 --- /dev/null +++ b/api/schemas/GiantBomb.json @@ -0,0 +1,11226 @@ +{ + "components": { + "parameters": { + "FieldList": { + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "$ref": "#/components/schemas/FieldList" + } + }, + "Filter": { + "description": "The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format)", + "in": "query", + "name": "filter", + "required": false, + "schema": { + "$ref": "#/components/schemas/Filter" + } + }, + "Format": { + "description": "The data format of the response takes either xml, json, or jsonp.", + "in": "query", + "name": "format", + "required": false, + "schema": { + "$ref": "#/components/schemas/Format" + } + }, + "Game": { + "description": "Filter by the ID field on the game resource.", + "in": "query", + "name": "game", + "required": false, + "schema": { + "$ref": "#/components/schemas/GameId" + } + }, + "Limit": { + "description": "The number of results to display per page. This value defaults to 100 and can not exceed this number.", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "$ref": "#/components/schemas/Limit" + } + }, + "Offset": { + "description": "Return results starting with the object at the offset specified.", + "in": "query", + "name": "offset", + "required": false, + "schema": { + "$ref": "#/components/schemas/Offset" + } + }, + "Page": { + "description": "Page number of search results.", + "in": "query", + "name": "page", + "required": false, + "schema": { + "$ref": "#/components/schemas/Page" + } + }, + "Platforms": { + "description": "Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by.", + "in": "query", + "name": "platforms", + "required": false, + "schema": { + "$ref": "#/components/schemas/PlatformId" + } + }, + "Query": { + "description": "The search string.", + "in": "query", + "name": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/Query" + } + }, + "Resources": { + "description": "List of resources to filter results. This filter can accept multiple arguments, each delimited with a \",\". Available options are:
game
franchise
character
concept
object
location
person
company
video", + "explode": false, + "in": "query", + "name": "resources", + "required": false, + "schema": { + "description": "##ENTER##", + "items": { + "$ref": "#/components/schemas/ResourceType" + }, + "type": "array" + }, + "style": "form" + }, + "Sort": { + "description": "The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc.", + "in": "query", + "name": "sort", + "required": false, + "schema": { + "$ref": "#/components/schemas/Sort" + } + }, + "SubscriberOnly": { + "description": "##ENTER##", + "in": "query", + "name": "subscriber_only", + "required": false, + "schema": { + "$ref": "#/components/schemas/SubscriberOnly" + } + }, + "TimeToSave": { + "description": "The number of seconds into the video the current user is", + "in": "query", + "name": "time_to_save", + "required": false, + "schema": { + "$ref": "#/components/schemas/TimeToSave" + } + }, + "VideoId": { + "description": "Id of the video", + "in": "query", + "name": "video_id", + "required": false, + "schema": { + "$ref": "#/components/schemas/VideoId" + } + } + }, + "responses": { + "InvalidAPIKey": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/jsonp": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + }, + "description": "##ENTER##" + } + }, + "schemas": { + "Accessory": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the accessory detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the accessory was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the accessory was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the accessory.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the accessory on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Accessory.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Accessory" + }, + { + "properties": {} + } + ] + }, + "Character": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the character is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the character detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "birthday": { + "description": "Birthday of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the character was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the character was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appeared_in_game": { + "description": "Game where the character made its first appearance.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "gender": { + "description": "Gender of the character. Available options are: Male, Female, Other", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "last_name": { + "description": "Last name of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "real_name": { + "description": "Real name of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the character on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Character.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Character" + }, + { + "properties": { + "concepts": { + "description": "Concepts related to the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "enemies": { + "description": "Enemeis of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "franchises": { + "description": "Franchises related to the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "friends": { + "description": "Friends of the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "games": { + "description": "Games the character has appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the character.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the character.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Chat": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the chat detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "channel_name": { + "description": "Name of the video streaming channel associated with the chat.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the chat.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for chat.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the chat.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the chat.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "password": { + "description": "chat password.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the chat on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "title": { + "description": "Title of the chat.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Chat.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Chat" + }, + { + "properties": {} + } + ] + }, + "Company": { + "description": "##ENTER##", + "properties": { + "abbreviation": { + "description": "Abbreviation of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "aliases": { + "description": "List of aliases the company is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the company detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the company was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_founded": { + "description": "Date the company was founded.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the company was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "location_address": { + "description": "Street address of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "location_city": { + "description": "City the company resides in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "location_country": { + "description": "Country the company resides in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "location_state": { + "description": "State the company resides in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "phone": { + "description": "Phone number of the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the company on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "website": { + "description": "URL to the company website.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Company.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Company" + }, + { + "properties": { + "characters": { + "description": "Characters related to the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "developed_games": { + "description": "Games the company has developed.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "developer_releases": { + "description": "Releases the company has developed.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "distributor_releases": { + "description": "Releases the company has distributed.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "published_games": { + "description": "Games published by the company.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "publisher_releases": { + "description": "Releases the company has published.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Concept": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the concept is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the concept detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the concept was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the concept was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appeared_in_franchise": { + "description": "Franchise where the concept made its first appearance.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appeared_in_game": { + "description": "Game where the concept made its first appearance.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the concept on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Concept.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Concept" + }, + { + "properties": { + "characters": { + "description": "Characters related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "franchises": { + "description": "Franchises related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "games": { + "description": "Games the concept has appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the concept.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "related_concepts": { + "description": "Other concepts related to the concept.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "CurrentLive": { + "description": "##ENTER##", + "properties": { + "success": { + "description": "##ENTER##", + "type": "integer" + }, + "video": { + "description": "##ENTER##", + "nullable": true, + "properties": { + "image": { + "description": "Thumbnail image of the video", + "example": "##WRONG TYPE##", + "type": "string" + }, + "stream": { + "description": "URL of the stream (HLS format)", + "example": "##WRONG TYPE##", + "type": "string" + }, + "title": { + "description": "Title of the live video", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "Dlc": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the dlc detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the dlc was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the dlc was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "game": { + "description": "Game the dlc is for.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "platform": { + "description": "The dlc's platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "release_date": { + "description": "Date of the dlc.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the dlc on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Dlc.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Dlc" + }, + { + "properties": {} + } + ] + }, + "FieldList": { + "description": "##ENTER##", + "type": "array" + }, + "Filter": { + "description": "##ENTER##", + "pattern": "^((\\w+:((\\w+)|(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}))),?)+$", + "type": "string" + }, + "Format": { + "description": "##ENTER##", + "enum": ["xml", "json", "jsonp"], + "type": "string" + }, + "Franchise": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the franchise is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the franchise detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the franchise was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the franchise was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the franchise on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Franchise.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Franchise" + }, + { + "properties": { + "characters": { + "description": "Characters related to the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "games": { + "description": "Games the franchise has appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the franchise.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Game": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the game is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the game detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the game was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the game was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_day": { + "description": "Expected day of release. The month is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_month": { + "description": "Expected month of release. The month is represented numerically. Combine with 'expected_release_day', 'expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_quarter": { + "description": "Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_year": { + "description": "Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "number_of_user_reviews": { + "description": "Number of user reviews of the game on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "original_game_rating": { + "description": "Rating of the first release of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "original_release_date": { + "description": "Date the game was first released.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "platforms": { + "description": "The platforms the game exists on.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the game on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Game.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Game" + }, + { + "properties": { + "characters": { + "description": "Characters related to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "developers": { + "description": "Companies who developed the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "dlcs": { + "description": "Game DLCs", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appearance_characters": { + "description": "Characters that first appeared in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appearance_concepts": { + "description": "Concepts that first appeared in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appearance_locations": { + "description": "Locations that first appeared in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appearance_objects": { + "description": "Objects that first appeared in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appearance_people": { + "description": "People that were first credited in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "franchises": { + "description": "Franchises related to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "genres": { + "description": "Genres that encompass the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "images": { + "description": "List of images associated to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "killed_characters": { + "description": "Characters killed in the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "publishers": { + "description": "Companies who published the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "releases": { + "description": "Releases of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "reviews": { + "description": "Staff reviews of the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "similar_games": { + "description": "Other games similar to the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "themes": { + "description": "Themes that encompass the game.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "videos": { + "description": "Videos associated to the game.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "GameId": { + "description": "##ENTER##", + "type": "integer" + }, + "GameRating": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the game_rating detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for game_rating.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the game_rating.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the game_rating.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the game_rating.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "rating_board": { + "description": "Rating board that issues this game_rating.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "GameRating.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/GameRating" + }, + { + "properties": {} + } + ] + }, + "Genre": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the genre detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the genre was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the genre was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the genre.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the genre on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Genre.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Genre" + }, + { + "properties": {} + } + ] + }, + "GetAllSavedTime": { + "properties": { + "savedTimes": { + "description": "##ENTER##", + "items": { + "description": "##ENTER##", + "properties": { + "savedOn": { + "description": "Time/date the progress was saved", + "example": "##WRONG TYPE##", + "type": "string" + }, + "savedTime": { + "description": "Saved time for this video", + "example": "##WRONG TYPE##", + "type": "string" + }, + "videoId": { + "description": "Id of the video", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "success": { + "description": "##ENTER##", + "type": "integer" + } + }, + "type": "object" + }, + "GetSavedTime": { + "description": "##ENTER##", + "properties": { + "savedTime": { + "description": "Saved time of the video or -1 if no time is saved for this user", + "example": "##WRONG TYPE##", + "type": "string" + }, + "success": { + "description": "##ENTER##", + "type": "integer" + } + }, + "type": "object" + }, + "Image": { + "description": "##ENTER##", + "properties": { + "icon_url": { + "description": "URL to the icon version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tag": { + "description": "Name of image tag for filerting images.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "medium_url": { + "description": "URL to the medium size of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "original_url": { + "description": "URL to the original image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "screen_large_url": { + "description": "URL to the large screenshot version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "screen_url": { + "description": "URL to the screenshot version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "small_url": { + "description": "URL to the small version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "super_url": { + "description": "URL to the super sized version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "thumb_url": { + "description": "URL to the thumb-sized version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "tiny_url": { + "description": "URL to the tiny version of the image.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "InvalidAPIKey": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "error": { + "enum": ["Invalid API Key"] + }, + "results": { + "enum": [[]] + }, + "status_code": { + "enum": [100] + } + } + } + ] + }, + "Limit": { + "description": "##ENTER##", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "Location": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the location is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the location detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the location was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the location was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appeared_in_game": { + "description": "Game where the location made its first appearance.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the location.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the location on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Location.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Location" + }, + { + "properties": {} + } + ] + }, + "Object": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the object is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the object detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the object was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the object was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_appeared_in_game": { + "description": "Game where the object made its first appearance.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the object on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Object.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Object" + }, + { + "properties": { + "characters": { + "description": "Characters related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "companies": { + "description": "Companies related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "franchises": { + "description": "Franchises related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "games": { + "description": "Games the object has appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the object.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the object.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Offset": { + "description": "##ENTER##", + "type": "integer" + }, + "Page": { + "description": "##ENTER##", + "type": "integer" + }, + "Person": { + "description": "##ENTER##", + "properties": { + "aliases": { + "description": "List of aliases the person is known by. A \\n (newline) seperates each alias.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the person detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "birth_date": { + "description": "Date the person was born.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "country": { + "description": "Country the person resides in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the person was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the person was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "death_date": { + "description": "Date the person died.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "first_credited_game": { + "description": "Game the person was first credited.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "gender": { + "description": "Gender of the person. Available options are: Male, Female, Other", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "hometown": { + "description": "City or town the person resides in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the person on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Person.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "properties": { + "characters": { + "description": "Characters related to the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "concepts": { + "description": "Concepts related to the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "franchises": { + "description": "Franchises related to the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "games": { + "description": "Games the person has appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "locations": { + "description": "Locations related to the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "objects": { + "description": "Objects related to the person.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "people": { + "description": "People who have worked with the person.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Platform": { + "description": "##ENTER##", + "properties": { + "abbreviation": { + "description": "Abbreviation of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the platform detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "company": { + "description": "Company that created the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the platform was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the platform was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image_tags": { + "description": "List of image tags to filter the images endpoint.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "install_base": { + "description": "Approximate number of sold hardware units.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "online_support": { + "description": "Flag indicating whether the platform has online capabilities.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "original_price": { + "description": "Initial price point of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "release_date": { + "description": "Date of the platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the platform on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Platform.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Platform" + }, + { + "properties": {} + } + ] + }, + "PlatformId": { + "description": "##ENTER##", + "type": "integer" + }, + "Promo": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the promo detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the promo was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the promo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for promo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the promo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the promo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "link": { + "description": "The link that promo points to.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the promo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "resource_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ResourceType" + }, + { + "description": "The type of resource the promo is pointing towards." + } + ] + }, + "user": { + "description": "Author of the promo.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Promo.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Promo" + }, + { + "properties": {} + } + ] + }, + "Query": { + "description": "##ENTER##", + "type": "string" + }, + "RatingBoard": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the rating_board detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the rating_board was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the rating_board was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the rating_board.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the rating_board on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "RatingBoard.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/RatingBoard" + }, + { + "properties": { + "region": { + "description": "Region the rating_board is responsible for.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Region": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the region detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the region was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the region was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the region.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the region on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Region.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Region" + }, + { + "properties": { + "rating_boards": { + "description": "region in the region.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "Release": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the release detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the release was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the release was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_day": { + "description": "\"Expected day of release. The day is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.\"", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_month": { + "description": "\"Expected month of release. The month is represented numerically. Combine with 'expected_release_day', expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.\"", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_quarter": { + "description": "Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "expected_release_year": { + "description": "Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'release_date' field is set.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "game": { + "description": "Game the release is for.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "game_rating": { + "description": "Rating of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "maximum_players": { + "description": "Maximum players", + "example": "##WRONG TYPE##", + "type": "string" + }, + "minimum_players": { + "description": "Minimum players", + "example": "##WRONG TYPE##", + "type": "string" + }, + "multiplayer_features": { + "description": "Multiplayer features", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "platform": { + "description": "The release's platform.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "product_code_type": { + "description": "The type of product code the release has (ex. UPC/A, ISBN-10, EAN-13).", + "example": "##WRONG TYPE##", + "type": "string" + }, + "product_code_value": { + "description": "The release's product code.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "region": { + "description": "Region the release is responsible for.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "release_date": { + "description": "Date of the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "resolutions": { + "description": "Resolutions available", + "example": "##WRONG TYPE##", + "type": "string" + }, + "singleplayer_features": { + "description": "Singleplayer features", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the release on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "sound_systems": { + "description": "Sound systems", + "example": "##WRONG TYPE##", + "type": "string" + }, + "widescreen_support": { + "description": "Widescreen support", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Release.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Release" + }, + { + "properties": { + "developers": { + "description": "Companies who developed the release.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "publishers": { + "description": "Companies who published the release.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "ResourceType": { + "description": "##ENTER##", + "enum": ["game", "franchise", "character", "concept", "object", "location", "person", "company", "video"], + "type": "string" + }, + "Response": { + "description": "##ENTER##", + "properties": { + "error": { + "description": "A text string representing the status_code", + "type": "string" + }, + "limit": { + "description": "The value of the limit filter specified, or 100 if not specified", + "type": "integer" + }, + "number_of_page_results": { + "description": "The number of results on this page", + "type": "integer" + }, + "number_of_total_results": { + "description": "The number of total results matching the filter conditions specified", + "type": "integer" + }, + "offset": { + "description": "The value of the offset filter specified, or 0 if not specified", + "type": "integer" + }, + "results": { + "description": "Zero or more items that match the filters specified", + "type": "string" + }, + "status_code": { + "description": "An integer indicating the result of the request. Acceptable values are:
1:OK
100:Invalid API Key
101:Object Not Found
102:Error in URL Format
103:'jsonp' format requires a 'json_callback' argument
104:Filter Error
105:Subscriber only video is for subscribers only", + "type": "integer" + } + }, + "type": "object", + "xml": { + "name": "response", + "wrapped": true + } + }, + "Review": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the review detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "dlc_name": { + "description": "Name of the Downloadable Content package.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "game": { + "description": "Game the review is for.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "publish_date": { + "description": "Date the review was published on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "release": { + "description": "Release of game for review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "reviewer": { + "description": "Name of the review's author.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "score": { + "description": "The score given to the game on a scale of 1 to 5.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the review on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Review.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Review" + }, + { + "properties": { + "platforms": { + "description": "Platforms the review appeared in.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "SaveTime": { + "description": "##ENTER##", + "properties": { + "message": { + "description": "##ENTER##", + "type": "string" + }, + "success": { + "description": "##ENTER##", + "type": "boolean" + } + }, + "type": "object" + }, + "Search": { + "allOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/Game" + }, + { + "$ref": "#/components/schemas/Franchise" + }, + { + "$ref": "#/components/schemas/Character" + }, + { + "$ref": "#/components/schemas/Concept" + }, + { + "$ref": "#/components/schemas/Object" + }, + { + "$ref": "#/components/schemas/Location" + }, + { + "$ref": "#/components/schemas/Person" + }, + { + "$ref": "#/components/schemas/Company" + }, + { + "$ref": "#/components/schemas/Video" + } + ] + }, + { + "description": "##ENTER##", + "properties": { + "resource_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ResourceType" + }, + { + "description": "The type of resource the result is mapped to. Available options are:
game
franchise
character
concept
object
location
person
company
video" + } + ] + } + }, + "type": "object" + } + ] + }, + "Sort": { + "description": "##ENTER##", + "pattern": "^\\w+:((asc)|(desc))$", + "type": "string" + }, + "SubscriberOnly": { + "description": "##ENTER##", + "type": "boolean" + }, + "Theme": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the theme detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for theme.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the theme.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the theme.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the theme on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Theme.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Theme" + }, + { + "properties": {} + } + ] + }, + "TimeToSave": { + "description": "##ENTER##", + "type": "integer" + }, + "Type": { + "description": "##ENTER##", + "properties": { + "detail_resource_name": { + "description": "The name of the type's detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the type.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "list_resource_name": { + "description": "The name of the type's list resource.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "UserReview": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the user_review detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_added": { + "description": "Date the user_review was added to Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "date_last_updated": { + "description": "Date the user_review was last updated on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the user_review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "description": { + "description": "Description of the user_review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "dlc": { + "description": "DLC being reviewed.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "game": { + "description": "Game being reviewd.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for user_review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the user_review.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "release": { + "description": "Release being reviewed.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "reviewer": { + "description": "Name of the review's author.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "score": { + "description": "The score given to the game on a scale of 1 to 5.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the user_review on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "UserReview.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/UserReview" + }, + { + "properties": {} + } + ] + }, + "Video": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the video detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "associations": { + "description": "Related objects to the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "embed_player": { + "description": "URL for video embed player. To be inserted into an iFrame.
You can add ?autoplay=true to auto-play.
You can add ?time=x where 'x' is an integer between 0 and the length of the video in seconds to start the video at that point.
You can add ?vol=x where 'x' is a decimal between 0 and 1, .75 for example, to set the starting volume.
The above three parameters may be used together. Example: ?time=45&vol=.5&autoplay=true
See http://www.giantbomb.com/api/video-embed-sample/ for more information on using the embed player.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "hd_url": { + "description": "URL to the HD version of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "high_url": { + "description": "URL to the High Res version of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "length_seconds": { + "description": "Length (in seconds) of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "low_url": { + "description": "URL to the Low Res version of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "premium": { + "description": "Premium status of video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "publish_date": { + "description": "Date the video was published on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "saved_time": { + "description": "The time where the user left off watching this video", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the video on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "url": { + "description": "The video's filename.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "user": { + "description": "Author of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "video_categories": { + "description": "Video categories", + "example": "##WRONG TYPE##", + "type": "string" + }, + "video_show": { + "description": "Video show", + "example": "##WRONG TYPE##", + "type": "string" + }, + "video_type": { + "description": "Video category", + "example": "##WRONG TYPE##", + "type": "string" + }, + "youtube_id": { + "description": "Youtube ID for the video.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "Video.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/Video" + }, + { + "properties": { + "crew": { + "description": "Crew of the video.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "hosts": { + "description": "Hosts of the video.", + "example": "##WRONG TYPE##", + "type": "string" + } + } + } + ] + }, + "VideoCategory": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the video_category detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the video_category.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the video_category.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the video_category.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the video_category.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the video_category on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "VideoCategory.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/VideoCategory" + }, + { + "properties": {} + } + ] + }, + "VideoId": { + "description": "##ENTER##", + "type": "integer" + }, + "VideoShow": { + "description": "##ENTER##", + "properties": { + "active": { + "description": "Is this show currently active", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_detail_url": { + "description": "URL pointing to the video_show detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "api_videos_url": { + "description": "Endpoint to retrieve the videos attached to this video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "display_nav": { + "description": "Should this show be displayed in navigation menus", + "example": "##WRONG TYPE##", + "type": "string" + }, + "guid": { + "description": "For use in single item api call for video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "image": { + "description": "Main image of the video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "latest": { + "description": "The latest episode of a video show. Overrides other sorts when used as a sort field.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "logo": { + "description": "Show logo.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "position": { + "description": "Editor ordering.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "premium": { + "description": "Premium status of video_show.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the video_show on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "title": { + "description": "Title of the video_show.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "VideoShow.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/VideoShow" + }, + { + "properties": {} + } + ] + }, + "VideoType": { + "description": "##ENTER##", + "properties": { + "api_detail_url": { + "description": "URL pointing to the video_type detail resource.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "deck": { + "description": "Brief summary of the video_type.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "id": { + "description": "Unique ID of the video_type.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "name": { + "description": "Name of the video_type.", + "example": "##WRONG TYPE##", + "type": "string" + }, + "site_detail_url": { + "description": "URL pointing to the video_type on Giant Bomb.", + "example": "##WRONG TYPE##", + "type": "string" + } + }, + "type": "object" + }, + "VideoType.Detail": { + "allOf": [ + { + "$ref": "#/components/schemas/VideoType" + }, + { + "properties": {} + } + ] + } + }, + "securitySchemes": { + "api_key": { + "in": "query", + "name": "api_key", + "type": "apiKey" + } + } + }, + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/" + }, + "info": { + "description": "##ENTER##", + "title": "Giant Bomb API", + "version": "0.7" + }, + "openapi": "3.0.2", + "paths": { + "/accessories": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Accessory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Accessory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Accessory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/accessory/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Accessory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Accessory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Accessory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/character/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "birthday", + "concepts", + "date_added", + "date_last_updated", + "deck", + "description", + "enemies", + "first_appeared_in_game", + "franchises", + "friends", + "games", + "gender", + "guid", + "id", + "image", + "image_tags", + "last_name", + "locations", + "name", + "objects", + "people", + "real_name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Character.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Character.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Character.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/characters": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "birthday", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_game", + "gender", + "guid", + "id", + "image", + "image_tags", + "last_name", + "name", + "real_name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Character" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Character" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Character" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/chat/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "channel_name", "deck", "guid", "id", "image", "password", "site_detail_url", "title"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Chat.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Chat.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Chat.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Live"] + }, + "summary": "##ENTER##" + }, + "/chats": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "channel_name", "deck", "guid", "id", "image", "password", "site_detail_url", "title"], + "type": "string" + } + } + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Chat" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Chat" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Chat" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Live"] + }, + "summary": "##ENTER##" + }, + "/companies": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "abbreviation", + "aliases", + "api_detail_url", + "date_added", + "date_founded", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "location_address", + "location_city", + "location_country", + "location_state", + "name", + "phone", + "site_detail_url", + "website" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Company" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Company" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Company" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/company/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "abbreviation", + "aliases", + "api_detail_url", + "characters", + "concepts", + "date_added", + "date_founded", + "date_last_updated", + "deck", + "description", + "developed_games", + "developer_releases", + "distributor_releases", + "guid", + "id", + "image", + "image_tags", + "location_address", + "location_city", + "location_country", + "location_state", + "locations", + "name", + "objects", + "people", + "phone", + "published_games", + "publisher_releases", + "site_detail_url", + "website" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Company.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Company.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Company.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/concept/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "characters", + "concepts", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_franchise", + "first_appeared_in_game", + "franchises", + "games", + "guid", + "id", + "image", + "image_tags", + "locations", + "name", + "objects", + "people", + "related_concepts", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Concept.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Concept.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Concept.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/concepts": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_franchise", + "first_appeared_in_game", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Concept" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Concept" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Concept" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/dlc/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "game", + "guid", + "id", + "image", + "name", + "platform", + "release_date", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Dlc.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Dlc.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Dlc.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/dlcs": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "game", + "guid", + "id", + "image", + "name", + "platform", + "release_date", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Platforms" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Dlc" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Dlc" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Dlc" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/franchise/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "characters", + "concepts", + "date_added", + "date_last_updated", + "deck", + "description", + "games", + "guid", + "id", + "image", + "image_tags", + "locations", + "name", + "objects", + "people", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Franchise.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Franchise.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Franchise.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/franchises": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Franchise" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Franchise" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Franchise" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/game_rating/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "guid", "id", "image", "name", "rating_board"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/GameRating.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/GameRating.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/GameRating.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/game_ratings": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "guid", "id", "image", "name", "rating_board"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/GameRating" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/GameRating" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/GameRating" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/game/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "characters", + "concepts", + "date_added", + "date_last_updated", + "deck", + "description", + "developers", + "dlcs", + "expected_release_day", + "expected_release_month", + "expected_release_quarter", + "expected_release_year", + "first_appearance_characters", + "first_appearance_concepts", + "first_appearance_locations", + "first_appearance_objects", + "first_appearance_people", + "franchises", + "genres", + "guid", + "id", + "image", + "image_tags", + "images", + "killed_characters", + "locations", + "name", + "number_of_user_reviews", + "objects", + "original_game_rating", + "original_release_date", + "people", + "platforms", + "publishers", + "releases", + "reviews", + "similar_games", + "site_detail_url", + "themes", + "videos" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Game.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Game.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Game.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/games": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "expected_release_day", + "expected_release_month", + "expected_release_quarter", + "expected_release_year", + "guid", + "id", + "image", + "image_tags", + "name", + "number_of_user_reviews", + "original_game_rating", + "original_release_date", + "platforms", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Platforms" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Game" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Game" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Game" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/genre/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Genre.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Genre.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Genre.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/genres": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Genre" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Genre" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Genre" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/images/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "icon_url", + "image_tag", + "medium_url", + "original_url", + "screen_large_url", + "screen_url", + "small_url", + "super_url", + "thumb_url", + "tiny_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Filter" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Image" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Image" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Image" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/location/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_game", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Location.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Location.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Location.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/locations": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_game", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Location" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Location" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Location" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/object/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "characters", + "companies", + "concepts", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_game", + "franchises", + "games", + "guid", + "id", + "image", + "image_tags", + "locations", + "name", + "objects", + "people", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Object.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Object.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Object.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/objects": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "first_appeared_in_game", + "guid", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Object" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Object" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Object" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/people": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "birth_date", + "country", + "date_added", + "date_last_updated", + "death_date", + "deck", + "description", + "first_credited_game", + "gender", + "guid", + "hometown", + "id", + "image", + "image_tags", + "name", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Person" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Person" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Person" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/person/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "aliases", + "api_detail_url", + "birth_date", + "characters", + "concepts", + "country", + "date_added", + "date_last_updated", + "death_date", + "deck", + "description", + "first_credited_game", + "franchises", + "games", + "gender", + "guid", + "hometown", + "id", + "image", + "image_tags", + "locations", + "name", + "objects", + "people", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Person.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Person.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Person.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/platform/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "abbreviation", + "api_detail_url", + "company", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "install_base", + "name", + "online_support", + "original_price", + "release_date", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Platform.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Platform.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Platform.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/platforms": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "abbreviation", + "api_detail_url", + "company", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "image_tags", + "install_base", + "name", + "online_support", + "original_price", + "release_date", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Platform" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Platform" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Platform" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/promo/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "deck", "guid", "id", "image", "link", "name", "resource_type", "user"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Promo.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Promo.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Promo.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["General"] + }, + "summary": "##ENTER##" + }, + "/promos": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "deck", "guid", "id", "image", "link", "name", "resource_type", "user"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Promo" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Promo" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Promo" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["General"] + }, + "summary": "##ENTER##" + }, + "/rating_board/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "name", + "region", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/RatingBoard.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/RatingBoard.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/RatingBoard.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/rating_boards": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/RatingBoard" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/RatingBoard" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/RatingBoard" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/region/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "guid", + "id", + "image", + "name", + "rating_boards", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Region.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Region.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Region.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/regions": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Region" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Region" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Region" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/release/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "developers", + "expected_release_day", + "expected_release_month", + "expected_release_quarter", + "expected_release_year", + "game", + "game_rating", + "guid", + "id", + "image", + "maximum_players", + "minimum_players", + "multiplayer_features", + "name", + "platform", + "product_code_type", + "product_code_value", + "publishers", + "region", + "release_date", + "resolutions", + "singleplayer_features", + "site_detail_url", + "sound_systems", + "widescreen_support" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Release.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Release.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Release.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/releases": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "expected_release_day", + "expected_release_month", + "expected_release_quarter", + "expected_release_year", + "game", + "game_rating", + "guid", + "id", + "image", + "maximum_players", + "minimum_players", + "multiplayer_features", + "name", + "platform", + "product_code_type", + "product_code_value", + "region", + "release_date", + "resolutions", + "singleplayer_features", + "site_detail_url", + "sound_systems", + "widescreen_support" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Platforms" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Release" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Release" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Release" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/review/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "deck", + "description", + "dlc_name", + "game", + "guid", + "id", + "platforms", + "publish_date", + "release", + "reviewer", + "score", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Review.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Review.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Review.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Reviews"] + }, + "summary": "##ENTER##" + }, + "/reviews": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "deck", + "description", + "dlc_name", + "game", + "guid", + "id", + "publish_date", + "release", + "reviewer", + "score", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Review" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Review" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Review" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Reviews"] + }, + "summary": "##ENTER##" + }, + "/search": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "abbreviation", + "aliases", + "api_detail_url", + "associations", + "birth_date", + "birthday", + "country", + "date_added", + "date_founded", + "date_last_updated", + "death_date", + "deck", + "description", + "embed_player", + "expected_release_day", + "expected_release_month", + "expected_release_quarter", + "expected_release_year", + "first_appeared_in_franchise", + "first_appeared_in_game", + "first_credited_game", + "gender", + "guid", + "hd_url", + "high_url", + "hometown", + "id", + "image", + "image_tags", + "last_name", + "length_seconds", + "location_address", + "location_city", + "location_country", + "location_state", + "low_url", + "name", + "number_of_user_reviews", + "original_game_rating", + "original_release_date", + "phone", + "platforms", + "premium", + "publish_date", + "real_name", + "resource_type", + "saved_time", + "site_detail_url", + "url", + "user", + "video_categories", + "video_show", + "video_type", + "website", + "youtube_id" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "The number of results to display per page. This value defaults to 10 and can not exceed this number.", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Limit" + }, + { + "description": "The number of results to display per page. This value defaults to 10 and can not exceed this number.", + "maximum": 10 + } + ] + } + }, + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/Query" + }, + { + "$ref": "#/components/parameters/Resources" + }, + { + "$ref": "#/components/parameters/SubscriberOnly" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Search" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Search" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Search" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Search"] + }, + "summary": "##ENTER##" + }, + "/theme/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "guid", "id", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Theme.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Theme.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Theme.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/themes": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "guid", "id", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Theme" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Theme" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Theme" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Wiki"] + }, + "summary": "##ENTER##" + }, + "/types": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Type" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Type" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Type" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["General"] + }, + "summary": "##ENTER##" + }, + "/user_review/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "dlc", + "game", + "guid", + "id", + "release", + "reviewer", + "score", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/UserReview.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/UserReview.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/UserReview.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Reviews"] + }, + "summary": "##ENTER##" + }, + "/user_reviews": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "date_added", + "date_last_updated", + "deck", + "description", + "dlc", + "game", + "guid", + "id", + "release", + "reviewer", + "score", + "site_detail_url" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Game" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/UserReview" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/UserReview" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/UserReview" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Reviews"] + }, + "summary": "##ENTER##" + }, + "/video_categories": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "deck", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Sort" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoCategory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoCategory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoCategory" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video_category/{id}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "deck", "id", "image", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoCategory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoCategory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoCategory.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video_show/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "active", + "api_detail_url", + "api_videos_url", + "deck", + "display_nav", + "guid", + "id", + "image", + "latest", + "logo", + "position", + "premium", + "site_detail_url", + "title" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoShow.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoShow.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoShow.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video_shows": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "active", + "api_detail_url", + "api_videos_url", + "deck", + "display_nav", + "guid", + "id", + "image", + "latest", + "logo", + "position", + "premium", + "site_detail_url", + "title" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoShow" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoShow" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoShow" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video_type/{id}": { + "description": "##ENTER##", + "get": { + "deprecated": true, + "description": "##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "deck", "id", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "id", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoType.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoType.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/VideoType.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Get a details of a video type", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video_types": { + "description": "##ENTER##", + "get": { + "deprecated": true, + "description": "##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": ["api_detail_url", "deck", "id", "name", "site_detail_url"], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoType" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoType" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/VideoType" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Get a list of video types", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video/{guid}": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "associations", + "crew", + "deck", + "embed_player", + "guid", + "hd_url", + "high_url", + "hosts", + "id", + "image", + "length_seconds", + "low_url", + "name", + "premium", + "publish_date", + "saved_time", + "site_detail_url", + "url", + "user", + "video_categories", + "video_show", + "video_type", + "youtube_id" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "description": "##ENTER##", + "in": "path", + "name": "guid", + "required": true, + "schema": { + "description": "##ENTER##", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Video.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Video.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "$ref": "#/components/schemas/Video.Detail" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + }, + "/video/current-live": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentLive" + } + }, + "application/jsonp": { + "schema": { + "$ref": "#/components/schemas/CurrentLive" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/CurrentLive" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Get the currently running live stream", + "tags": ["Live"] + }, + "summary": "##ENTER##" + }, + "/video/get-all-saved-times": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllSavedTime" + } + }, + "application/jsonp": { + "schema": { + "$ref": "#/components/schemas/GetAllSavedTime" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/GetAllSavedTime" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Get all the video saved times for the user", + "tags": ["Bookmarks"] + }, + "summary": "##ENTER##" + }, + "/video/get-saved-time": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "$ref": "#/components/parameters/VideoId" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSavedTime" + } + }, + "application/jsonp": { + "schema": { + "$ref": "#/components/schemas/GetSavedTime" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/GetSavedTime" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Get the saved time of a video for the user", + "tags": ["Bookmarks"] + }, + "summary": "##ENTER##" + }, + "/video/save-time": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "$ref": "#/components/parameters/VideoId" + }, + { + "$ref": "#/components/parameters/TimeToSave" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveTime" + } + }, + "application/jsonp": { + "schema": { + "$ref": "#/components/schemas/SaveTime" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/SaveTime" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "Save the progress time of a video for this user", + "tags": ["Bookmarks"] + }, + "summary": "##ENTER##" + }, + "/videos": { + "description": "##ENTER##", + "get": { + "description": "##ENTER##", + "externalDocs": { + "description": "##ENTER##", + "url": "https://www.giantbomb.com/api/documentation/#ADD" + }, + "parameters": [ + { + "$ref": "#/components/parameters/Format" + }, + { + "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", + "explode": false, + "in": "query", + "name": "field_list", + "required": false, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldList" + }, + { + "items": { + "enum": [ + "api_detail_url", + "associations", + "deck", + "embed_player", + "guid", + "hd_url", + "high_url", + "id", + "image", + "length_seconds", + "low_url", + "name", + "premium", + "publish_date", + "saved_time", + "site_detail_url", + "url", + "user", + "video_categories", + "video_show", + "video_type", + "youtube_id" + ], + "type": "string" + } + } + ] + }, + "style": "form" + }, + { + "$ref": "#/components/parameters/Limit" + }, + { + "$ref": "#/components/parameters/Offset" + }, + { + "$ref": "#/components/parameters/Sort" + }, + { + "$ref": "#/components/parameters/SubscriberOnly" + }, + { + "$ref": "#/components/parameters/Filter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Video" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/jsonp": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Video" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/Video" + }, + "type": "array" + }, + "version": { + "description": "##ENTER##", + "example": "1.0", + "type": "string" + } + }, + "type": "object" + } + ], + "type": "object" + } + } + }, + "description": "##ENTER##" + }, + "401": { + "$ref": "#/components/responses/InvalidAPIKey" + } + }, + "security": [ + { + "api_key": [] + } + ], + "summary": "##ENTER##", + "tags": ["Videos"] + }, + "summary": "##ENTER##" + } + }, + "servers": [ + { + "description": "##ENTER##", + "url": "http://www.giantbomb.com/api/" + } + ], + "tags": [ + { + "description": "##ENTER##", + "name": "General" + }, + { + "description": "##ENTER##", + "name": "Wiki" + }, + { + "description": "##ENTER##", + "name": "Search" + }, + { + "description": "##ENTER##", + "name": "Reviews" + }, + { + "description": "##ENTER##", + "name": "Videos" + }, + { + "description": "##ENTER##", + "name": "Live" + }, + { + "description": "##ENTER##", + "name": "Bookmarks" + } + ] +} diff --git a/api/schemas/GiantBomb.ts b/api/schemas/GiantBomb.ts new file mode 100644 index 00000000..9d8f4ee8 --- /dev/null +++ b/api/schemas/GiantBomb.ts @@ -0,0 +1,6454 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/accessories': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Accessory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Accessory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Accessory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/accessory/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Accessory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Accessory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Accessory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/character/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Character.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Character.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Character.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Character'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Character'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Character'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/chat/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Chat.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Chat.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Chat.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/chats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Chat'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Chat'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Chat'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/companies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Company'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Company'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Company'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/company/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Company.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Company.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Company.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/concept/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Concept.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Concept.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Concept.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/concepts': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Concept'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Concept'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Concept'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/dlc/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Dlc.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Dlc.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Dlc.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/dlcs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ + platforms?: components['parameters']['Platforms']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Dlc'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Dlc'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Dlc'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/franchise/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Franchise.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Franchise.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Franchise.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/franchises': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Franchise'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Franchise'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Franchise'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/game_rating/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['GameRating.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['GameRating.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['GameRating.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/game_ratings': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['GameRating'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['GameRating'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['GameRating'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/game/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Game.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Game.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Game.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/games': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ + platforms?: components['parameters']['Platforms']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Game'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Game'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Game'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/genre/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Genre.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Genre.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Genre.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/genres': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Genre'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Genre'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Genre'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/images/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Image'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Image'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Image'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/location/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Location.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Location.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Location.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/locations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Location'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Location'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Location'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/object/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Object.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Object.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Object.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/objects': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Object'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Object'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Object'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Person'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Person'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Person'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/person/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Person.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Person.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Person.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/platform/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Platform.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Platform.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Platform.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/platforms': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Platform'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Platform'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Platform'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/promo/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Promo.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Promo.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Promo.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/promos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Promo'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Promo'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Promo'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/rating_board/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/rating_boards': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['RatingBoard'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/region/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Region.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Region.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Region.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/regions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Region'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Region'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Region'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/release/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Release.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Release.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Release.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/releases': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ + platforms?: components['parameters']['Platforms']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Release'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Release'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Release'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/review/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Review.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Review.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Review.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Review'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Review'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Review'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 10 and can not exceed this number. */ + limit?: components['schemas']['Limit'] & unknown; + /** @description Page number of search results. */ + page?: components['parameters']['Page']; + /** @description The search string. */ + query?: components['parameters']['Query']; + /** @description List of resources to filter results. This filter can accept multiple arguments, each delimited with a ",". Available options are:
game
franchise
character
concept
object
location
person
company
video */ + resources?: components['parameters']['Resources']; + /** @description ##ENTER## */ + subscriber_only?: components['parameters']['SubscriberOnly']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Search'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Search'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Search'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/theme/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Theme.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Theme.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Theme.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/themes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Theme'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Theme'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Theme'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/types': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Type'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Type'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Type'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/user_review/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['UserReview.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['UserReview.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['UserReview.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/user_reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description Filter by the ID field on the game resource. */ + game?: components['parameters']['Game']; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['UserReview'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['UserReview'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['UserReview'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_categories': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_category/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoCategory.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_show/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_shows': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoShow'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_type/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a details of a video type + * @deprecated + * @description ##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoType.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoType.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoType.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video_types': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a list of video types + * @deprecated + * @description ##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['VideoType'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['VideoType'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['VideoType'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video/{guid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + }; + header?: never; + path: { + /** @description ##ENTER## */ + guid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Video.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Video.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Video.Detail']; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video/current-live': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get the currently running live stream + * @description ##ENTER## + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['CurrentLive']; + 'application/jsonp': components['schemas']['CurrentLive']; + 'application/xml': components['schemas']['CurrentLive']; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video/get-all-saved-times': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all the video saved times for the user + * @description ##ENTER## + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetAllSavedTime']; + 'application/jsonp': components['schemas']['GetAllSavedTime']; + 'application/xml': components['schemas']['GetAllSavedTime']; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video/get-saved-time': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get the saved time of a video for the user + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description Id of the video */ + video_id?: components['parameters']['VideoId']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetSavedTime']; + 'application/jsonp': components['schemas']['GetSavedTime']; + 'application/xml': components['schemas']['GetSavedTime']; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/video/save-time': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Save the progress time of a video for this user + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description Id of the video */ + video_id?: components['parameters']['VideoId']; + /** @description The number of seconds into the video the current user is */ + time_to_save?: components['parameters']['TimeToSave']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SaveTime']; + 'application/jsonp': components['schemas']['SaveTime']; + 'application/xml': components['schemas']['SaveTime']; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ##ENTER## + * @description ##ENTER## + */ + get: { + parameters: { + query?: { + /** @description The data format of the response takes either xml, json, or jsonp. */ + format?: components['parameters']['Format']; + /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ + field_list?: components['schemas']['FieldList'] & unknown; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + limit?: components['parameters']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + offset?: components['parameters']['Offset']; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + sort?: components['parameters']['Sort']; + /** @description ##ENTER## */ + subscriber_only?: components['parameters']['SubscriberOnly']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + filter?: components['parameters']['Filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ##ENTER## */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Response'] & { + results?: components['schemas']['Video'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/jsonp': components['schemas']['Response'] & { + results?: components['schemas']['Video'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + 'application/xml': components['schemas']['Response'] & { + results?: components['schemas']['Video'][]; + /** + * @description ##ENTER## + * @example 1.0 + */ + version?: string; + }; + }; + }; + 401: components['responses']['InvalidAPIKey']; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description ##ENTER## */ + Accessory: { + /** + * @description URL pointing to the accessory detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the accessory was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the accessory was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the accessory. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the accessory. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for accessory. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the accessory. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the accessory. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the accessory. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the accessory on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Accessory.Detail': components['schemas']['Accessory'] & unknown; + /** @description ##ENTER## */ + Character: { + /** + * @description List of aliases the character is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the character detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Birthday of the character. + * @example ##WRONG TYPE## + */ + birthday?: string; + /** + * @description Date the character was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the character was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the character. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the character. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Game where the character made its first appearance. + * @example ##WRONG TYPE## + */ + first_appeared_in_game?: string; + /** + * @description Gender of the character. Available options are: Male, Female, Other + * @example ##WRONG TYPE## + */ + gender?: string; + /** + * @description For use in single item api call for character. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the character. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the character. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Last name of the character. + * @example ##WRONG TYPE## + */ + last_name?: string; + /** + * @description Name of the character. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Real name of the character. + * @example ##WRONG TYPE## + */ + real_name?: string; + /** + * @description URL pointing to the character on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Character.Detail': components['schemas']['Character'] & { + /** + * @description Concepts related to the character. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Enemeis of the character. + * @example ##WRONG TYPE## + */ + enemies?: string; + /** + * @description Franchises related to the character. + * @example ##WRONG TYPE## + */ + franchises?: string; + /** + * @description Friends of the character. + * @example ##WRONG TYPE## + */ + friends?: string; + /** + * @description Games the character has appeared in. + * @example ##WRONG TYPE## + */ + games?: string; + /** + * @description Locations related to the character. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the character. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the character. + * @example ##WRONG TYPE## + */ + people?: string; + }; + /** @description ##ENTER## */ + Chat: { + /** + * @description URL pointing to the chat detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Name of the video streaming channel associated with the chat. + * @example ##WRONG TYPE## + */ + channel_name?: string; + /** + * @description Brief summary of the chat. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description For use in single item api call for chat. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the chat. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the chat. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description chat password. + * @example ##WRONG TYPE## + */ + password?: string; + /** + * @description URL pointing to the chat on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + /** + * @description Title of the chat. + * @example ##WRONG TYPE## + */ + title?: string; + }; + 'Chat.Detail': components['schemas']['Chat'] & unknown; + /** @description ##ENTER## */ + Company: { + /** + * @description Abbreviation of the company. + * @example ##WRONG TYPE## + */ + abbreviation?: string; + /** + * @description List of aliases the company is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the company detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the company was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the company was founded. + * @example ##WRONG TYPE## + */ + date_founded?: string; + /** + * @description Date the company was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the company. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the company. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for company. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the company. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the company. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Street address of the company. + * @example ##WRONG TYPE## + */ + location_address?: string; + /** + * @description City the company resides in. + * @example ##WRONG TYPE## + */ + location_city?: string; + /** + * @description Country the company resides in. + * @example ##WRONG TYPE## + */ + location_country?: string; + /** + * @description State the company resides in. + * @example ##WRONG TYPE## + */ + location_state?: string; + /** + * @description Name of the company. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Phone number of the company. + * @example ##WRONG TYPE## + */ + phone?: string; + /** + * @description URL pointing to the company on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + /** + * @description URL to the company website. + * @example ##WRONG TYPE## + */ + website?: string; + }; + 'Company.Detail': components['schemas']['Company'] & { + /** + * @description Characters related to the company. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Concepts related to the company. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Games the company has developed. + * @example ##WRONG TYPE## + */ + developed_games?: string; + /** + * @description Releases the company has developed. + * @example ##WRONG TYPE## + */ + developer_releases?: string; + /** + * @description Releases the company has distributed. + * @example ##WRONG TYPE## + */ + distributor_releases?: string; + /** + * @description Locations related to the company. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the company. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the company. + * @example ##WRONG TYPE## + */ + people?: string; + /** + * @description Games published by the company. + * @example ##WRONG TYPE## + */ + published_games?: string; + /** + * @description Releases the company has published. + * @example ##WRONG TYPE## + */ + publisher_releases?: string; + }; + /** @description ##ENTER## */ + Concept: { + /** + * @description List of aliases the concept is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the concept detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the concept was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the concept was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the concept. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the concept. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Franchise where the concept made its first appearance. + * @example ##WRONG TYPE## + */ + first_appeared_in_franchise?: string; + /** + * @description Game where the concept made its first appearance. + * @example ##WRONG TYPE## + */ + first_appeared_in_game?: string; + /** + * @description For use in single item api call for concept. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the concept. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the concept. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the concept. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the concept on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Concept.Detail': components['schemas']['Concept'] & { + /** + * @description Characters related to the concept. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Concepts related to the concept. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Franchises related to the concept. + * @example ##WRONG TYPE## + */ + franchises?: string; + /** + * @description Games the concept has appeared in. + * @example ##WRONG TYPE## + */ + games?: string; + /** + * @description Locations related to the concept. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the concept. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the concept. + * @example ##WRONG TYPE## + */ + people?: string; + /** + * @description Other concepts related to the concept. + * @example ##WRONG TYPE## + */ + related_concepts?: string; + }; + /** @description ##ENTER## */ + CurrentLive: { + /** @description ##ENTER## */ + success?: number; + /** @description ##ENTER## */ + video?: { + /** + * @description Thumbnail image of the video + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description URL of the stream (HLS format) + * @example ##WRONG TYPE## + */ + stream?: string; + /** + * @description Title of the live video + * @example ##WRONG TYPE## + */ + title?: string; + } | null; + }; + /** @description ##ENTER## */ + Dlc: { + /** + * @description URL pointing to the dlc detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the dlc was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the dlc was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the dlc. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the dlc. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Game the dlc is for. + * @example ##WRONG TYPE## + */ + game?: string; + /** + * @description For use in single item api call for dlc. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the dlc. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the dlc. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the dlc. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description The dlc's platform. + * @example ##WRONG TYPE## + */ + platform?: string; + /** + * @description Date of the dlc. + * @example ##WRONG TYPE## + */ + release_date?: string; + /** + * @description URL pointing to the dlc on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Dlc.Detail': components['schemas']['Dlc'] & unknown; + /** @description ##ENTER## */ + FieldList: unknown[]; + /** @description ##ENTER## */ + Filter: string; + /** + * @description ##ENTER## + * @enum {string} + */ + Format: 'xml' | 'json' | 'jsonp'; + /** @description ##ENTER## */ + Franchise: { + /** + * @description List of aliases the franchise is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the franchise detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the franchise was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the franchise was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the franchise. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the franchise. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for franchise. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the franchise. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the franchise. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the franchise. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the franchise on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Franchise.Detail': components['schemas']['Franchise'] & { + /** + * @description Characters related to the franchise. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Concepts related to the franchise. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Games the franchise has appeared in. + * @example ##WRONG TYPE## + */ + games?: string; + /** + * @description Locations related to the franchise. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the franchise. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the franchise. + * @example ##WRONG TYPE## + */ + people?: string; + }; + /** @description ##ENTER## */ + Game: { + /** + * @description List of aliases the game is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the game detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the game was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the game was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the game. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the game. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Expected day of release. The month is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_day?: string; + /** + * @description Expected month of release. The month is represented numerically. Combine with 'expected_release_day', 'expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_month?: string; + /** + * @description Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_quarter?: string; + /** + * @description Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_year?: string; + /** + * @description For use in single item api call for game. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the game. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the game. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the game. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Number of user reviews of the game on Giant Bomb. + * @example ##WRONG TYPE## + */ + number_of_user_reviews?: string; + /** + * @description Rating of the first release of the game. + * @example ##WRONG TYPE## + */ + original_game_rating?: string; + /** + * @description Date the game was first released. + * @example ##WRONG TYPE## + */ + original_release_date?: string; + /** + * @description The platforms the game exists on. + * @example ##WRONG TYPE## + */ + platforms?: string; + /** + * @description URL pointing to the game on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Game.Detail': components['schemas']['Game'] & { + /** + * @description Characters related to the game. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Concepts related to the game. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Companies who developed the game. + * @example ##WRONG TYPE## + */ + developers?: string; + /** + * @description Game DLCs + * @example ##WRONG TYPE## + */ + dlcs?: string; + /** + * @description Characters that first appeared in the game. + * @example ##WRONG TYPE## + */ + first_appearance_characters?: string; + /** + * @description Concepts that first appeared in the game. + * @example ##WRONG TYPE## + */ + first_appearance_concepts?: string; + /** + * @description Locations that first appeared in the game. + * @example ##WRONG TYPE## + */ + first_appearance_locations?: string; + /** + * @description Objects that first appeared in the game. + * @example ##WRONG TYPE## + */ + first_appearance_objects?: string; + /** + * @description People that were first credited in the game. + * @example ##WRONG TYPE## + */ + first_appearance_people?: string; + /** + * @description Franchises related to the game. + * @example ##WRONG TYPE## + */ + franchises?: string; + /** + * @description Genres that encompass the game. + * @example ##WRONG TYPE## + */ + genres?: string; + /** + * @description List of images associated to the game. + * @example ##WRONG TYPE## + */ + images?: string; + /** + * @description Characters killed in the game. + * @example ##WRONG TYPE## + */ + killed_characters?: string; + /** + * @description Locations related to the game. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the game. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the game. + * @example ##WRONG TYPE## + */ + people?: string; + /** + * @description Companies who published the game. + * @example ##WRONG TYPE## + */ + publishers?: string; + /** + * @description Releases of the game. + * @example ##WRONG TYPE## + */ + releases?: string; + /** + * @description Staff reviews of the game. + * @example ##WRONG TYPE## + */ + reviews?: string; + /** + * @description Other games similar to the game. + * @example ##WRONG TYPE## + */ + similar_games?: string; + /** + * @description Themes that encompass the game. + * @example ##WRONG TYPE## + */ + themes?: string; + /** + * @description Videos associated to the game. + * @example ##WRONG TYPE## + */ + videos?: string; + }; + /** @description ##ENTER## */ + GameId: number; + /** @description ##ENTER## */ + GameRating: { + /** + * @description URL pointing to the game_rating detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description For use in single item api call for game_rating. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the game_rating. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the game_rating. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the game_rating. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Rating board that issues this game_rating. + * @example ##WRONG TYPE## + */ + rating_board?: string; + }; + 'GameRating.Detail': components['schemas']['GameRating'] & unknown; + /** @description ##ENTER## */ + Genre: { + /** + * @description URL pointing to the genre detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the genre was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the genre was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the genre. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the genre. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for genre. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the genre. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the genre. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the genre. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the genre on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Genre.Detail': components['schemas']['Genre'] & unknown; + GetAllSavedTime: { + /** @description ##ENTER## */ + savedTimes?: { + /** + * @description Time/date the progress was saved + * @example ##WRONG TYPE## + */ + savedOn?: string; + /** + * @description Saved time for this video + * @example ##WRONG TYPE## + */ + savedTime?: string; + /** + * @description Id of the video + * @example ##WRONG TYPE## + */ + videoId?: string; + }[]; + /** @description ##ENTER## */ + success?: number; + }; + /** @description ##ENTER## */ + GetSavedTime: { + /** + * @description Saved time of the video or -1 if no time is saved for this user + * @example ##WRONG TYPE## + */ + savedTime?: string; + /** @description ##ENTER## */ + success?: number; + }; + /** @description ##ENTER## */ + Image: { + /** + * @description URL to the icon version of the image. + * @example ##WRONG TYPE## + */ + icon_url?: string; + /** + * @description Name of image tag for filerting images. + * @example ##WRONG TYPE## + */ + image_tag?: string; + /** + * @description URL to the medium size of the image. + * @example ##WRONG TYPE## + */ + medium_url?: string; + /** + * @description URL to the original image. + * @example ##WRONG TYPE## + */ + original_url?: string; + /** + * @description URL to the large screenshot version of the image. + * @example ##WRONG TYPE## + */ + screen_large_url?: string; + /** + * @description URL to the screenshot version of the image. + * @example ##WRONG TYPE## + */ + screen_url?: string; + /** + * @description URL to the small version of the image. + * @example ##WRONG TYPE## + */ + small_url?: string; + /** + * @description URL to the super sized version of the image. + * @example ##WRONG TYPE## + */ + super_url?: string; + /** + * @description URL to the thumb-sized version of the image. + * @example ##WRONG TYPE## + */ + thumb_url?: string; + /** + * @description URL to the tiny version of the image. + * @example ##WRONG TYPE## + */ + tiny_url?: string; + }; + InvalidAPIKey: components['schemas']['Response'] & { + /** @enum {unknown} */ + error?: 'Invalid API Key'; + /** @enum {unknown} */ + results?: never[]; + /** @enum {unknown} */ + status_code?: 100; + }; + /** @description ##ENTER## */ + Limit: number; + /** @description ##ENTER## */ + Location: { + /** + * @description List of aliases the location is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the location detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the location was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the location was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the location. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the location. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Game where the location made its first appearance. + * @example ##WRONG TYPE## + */ + first_appeared_in_game?: string; + /** + * @description For use in single item api call for location. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the location. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the location. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the location. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the location on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Location.Detail': components['schemas']['Location'] & unknown; + /** @description ##ENTER## */ + Object: { + /** + * @description List of aliases the object is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the object detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the object was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the object was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the object. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the object. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Game where the object made its first appearance. + * @example ##WRONG TYPE## + */ + first_appeared_in_game?: string; + /** + * @description For use in single item api call for object. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the object. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the object. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the object. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the object on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Object.Detail': components['schemas']['Object'] & { + /** + * @description Characters related to the object. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Companies related to the object. + * @example ##WRONG TYPE## + */ + companies?: string; + /** + * @description Concepts related to the object. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Franchises related to the object. + * @example ##WRONG TYPE## + */ + franchises?: string; + /** + * @description Games the object has appeared in. + * @example ##WRONG TYPE## + */ + games?: string; + /** + * @description Locations related to the object. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the object. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the object. + * @example ##WRONG TYPE## + */ + people?: string; + }; + /** @description ##ENTER## */ + Offset: number; + /** @description ##ENTER## */ + Page: number; + /** @description ##ENTER## */ + Person: { + /** + * @description List of aliases the person is known by. A \n (newline) seperates each alias. + * @example ##WRONG TYPE## + */ + aliases?: string; + /** + * @description URL pointing to the person detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the person was born. + * @example ##WRONG TYPE## + */ + birth_date?: string; + /** + * @description Country the person resides in. + * @example ##WRONG TYPE## + */ + country?: string; + /** + * @description Date the person was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the person was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Date the person died. + * @example ##WRONG TYPE## + */ + death_date?: string; + /** + * @description Brief summary of the person. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the person. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Game the person was first credited. + * @example ##WRONG TYPE## + */ + first_credited_game?: string; + /** + * @description Gender of the person. Available options are: Male, Female, Other + * @example ##WRONG TYPE## + */ + gender?: string; + /** + * @description For use in single item api call for person. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description City or town the person resides in. + * @example ##WRONG TYPE## + */ + hometown?: string; + /** + * @description Unique ID of the person. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the person. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Name of the person. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the person on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Person.Detail': components['schemas']['Person'] & { + /** + * @description Characters related to the person. + * @example ##WRONG TYPE## + */ + characters?: string; + /** + * @description Concepts related to the person. + * @example ##WRONG TYPE## + */ + concepts?: string; + /** + * @description Franchises related to the person. + * @example ##WRONG TYPE## + */ + franchises?: string; + /** + * @description Games the person has appeared in. + * @example ##WRONG TYPE## + */ + games?: string; + /** + * @description Locations related to the person. + * @example ##WRONG TYPE## + */ + locations?: string; + /** + * @description Objects related to the person. + * @example ##WRONG TYPE## + */ + objects?: string; + /** + * @description People who have worked with the person. + * @example ##WRONG TYPE## + */ + people?: string; + }; + /** @description ##ENTER## */ + Platform: { + /** + * @description Abbreviation of the platform. + * @example ##WRONG TYPE## + */ + abbreviation?: string; + /** + * @description URL pointing to the platform detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Company that created the platform. + * @example ##WRONG TYPE## + */ + company?: string; + /** + * @description Date the platform was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the platform was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the platform. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the platform. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for platform. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the platform. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the platform. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description List of image tags to filter the images endpoint. + * @example ##WRONG TYPE## + */ + image_tags?: string; + /** + * @description Approximate number of sold hardware units. + * @example ##WRONG TYPE## + */ + install_base?: string; + /** + * @description Name of the platform. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Flag indicating whether the platform has online capabilities. + * @example ##WRONG TYPE## + */ + online_support?: string; + /** + * @description Initial price point of the platform. + * @example ##WRONG TYPE## + */ + original_price?: string; + /** + * @description Date of the platform. + * @example ##WRONG TYPE## + */ + release_date?: string; + /** + * @description URL pointing to the platform on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Platform.Detail': components['schemas']['Platform'] & unknown; + /** @description ##ENTER## */ + PlatformId: number; + /** @description ##ENTER## */ + Promo: { + /** + * @description URL pointing to the promo detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the promo was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Brief summary of the promo. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description For use in single item api call for promo. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the promo. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the promo. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description The link that promo points to. + * @example ##WRONG TYPE## + */ + link?: string; + /** + * @description Name of the promo. + * @example ##WRONG TYPE## + */ + name?: string; + resource_type?: components['schemas']['ResourceType'] & unknown; + /** + * @description Author of the promo. + * @example ##WRONG TYPE## + */ + user?: string; + }; + 'Promo.Detail': components['schemas']['Promo'] & unknown; + /** @description ##ENTER## */ + Query: string; + /** @description ##ENTER## */ + RatingBoard: { + /** + * @description URL pointing to the rating_board detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the rating_board was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the rating_board was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the rating_board. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the rating_board. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for rating_board. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the rating_board. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the rating_board. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the rating_board. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the rating_board on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'RatingBoard.Detail': components['schemas']['RatingBoard'] & { + /** + * @description Region the rating_board is responsible for. + * @example ##WRONG TYPE## + */ + region?: string; + }; + /** @description ##ENTER## */ + Region: { + /** + * @description URL pointing to the region detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the region was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the region was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the region. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the region. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description For use in single item api call for region. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the region. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the region. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the region. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the region on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Region.Detail': components['schemas']['Region'] & { + /** + * @description region in the region. + * @example ##WRONG TYPE## + */ + rating_boards?: string; + }; + /** @description ##ENTER## */ + Release: { + /** + * @description URL pointing to the release detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the release was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the release was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the release. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the release. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description "Expected day of release. The day is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set." + * @example ##WRONG TYPE## + */ + expected_release_day?: string; + /** + * @description "Expected month of release. The month is represented numerically. Combine with 'expected_release_day', expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set." + * @example ##WRONG TYPE## + */ + expected_release_month?: string; + /** + * @description Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_quarter?: string; + /** + * @description Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'release_date' field is set. + * @example ##WRONG TYPE## + */ + expected_release_year?: string; + /** + * @description Game the release is for. + * @example ##WRONG TYPE## + */ + game?: string; + /** + * @description Rating of the release. + * @example ##WRONG TYPE## + */ + game_rating?: string; + /** + * @description For use in single item api call for release. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the release. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the release. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Maximum players + * @example ##WRONG TYPE## + */ + maximum_players?: string; + /** + * @description Minimum players + * @example ##WRONG TYPE## + */ + minimum_players?: string; + /** + * @description Multiplayer features + * @example ##WRONG TYPE## + */ + multiplayer_features?: string; + /** + * @description Name of the release. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description The release's platform. + * @example ##WRONG TYPE## + */ + platform?: string; + /** + * @description The type of product code the release has (ex. UPC/A, ISBN-10, EAN-13). + * @example ##WRONG TYPE## + */ + product_code_type?: string; + /** + * @description The release's product code. + * @example ##WRONG TYPE## + */ + product_code_value?: string; + /** + * @description Region the release is responsible for. + * @example ##WRONG TYPE## + */ + region?: string; + /** + * @description Date of the release. + * @example ##WRONG TYPE## + */ + release_date?: string; + /** + * @description Resolutions available + * @example ##WRONG TYPE## + */ + resolutions?: string; + /** + * @description Singleplayer features + * @example ##WRONG TYPE## + */ + singleplayer_features?: string; + /** + * @description URL pointing to the release on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + /** + * @description Sound systems + * @example ##WRONG TYPE## + */ + sound_systems?: string; + /** + * @description Widescreen support + * @example ##WRONG TYPE## + */ + widescreen_support?: string; + }; + 'Release.Detail': components['schemas']['Release'] & { + /** + * @description Companies who developed the release. + * @example ##WRONG TYPE## + */ + developers?: string; + /** + * @description Companies who published the release. + * @example ##WRONG TYPE## + */ + publishers?: string; + }; + /** + * @description ##ENTER## + * @enum {string} + */ + ResourceType: 'game' | 'franchise' | 'character' | 'concept' | 'object' | 'location' | 'person' | 'company' | 'video'; + /** @description ##ENTER## */ + Response: { + /** @description A text string representing the status_code */ + error?: string; + /** @description The value of the limit filter specified, or 100 if not specified */ + limit?: number; + /** @description The number of results on this page */ + number_of_page_results?: number; + /** @description The number of total results matching the filter conditions specified */ + number_of_total_results?: number; + /** @description The value of the offset filter specified, or 0 if not specified */ + offset?: number; + /** @description Zero or more items that match the filters specified */ + results?: string; + /** @description An integer indicating the result of the request. Acceptable values are:
1:OK
100:Invalid API Key
101:Object Not Found
102:Error in URL Format
103:'jsonp' format requires a 'json_callback' argument
104:Filter Error
105:Subscriber only video is for subscribers only */ + status_code?: number; + }; + /** @description ##ENTER## */ + Review: { + /** + * @description URL pointing to the review detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Brief summary of the review. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the review. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description Name of the Downloadable Content package. + * @example ##WRONG TYPE## + */ + dlc_name?: string; + /** + * @description Game the review is for. + * @example ##WRONG TYPE## + */ + game?: string; + /** + * @description For use in single item api call for review. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the review. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Date the review was published on Giant Bomb. + * @example ##WRONG TYPE## + */ + publish_date?: string; + /** + * @description Release of game for review. + * @example ##WRONG TYPE## + */ + release?: string; + /** + * @description Name of the review's author. + * @example ##WRONG TYPE## + */ + reviewer?: string; + /** + * @description The score given to the game on a scale of 1 to 5. + * @example ##WRONG TYPE## + */ + score?: string; + /** + * @description URL pointing to the review on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Review.Detail': components['schemas']['Review'] & { + /** + * @description Platforms the review appeared in. + * @example ##WRONG TYPE## + */ + platforms?: string; + }; + /** @description ##ENTER## */ + SaveTime: { + /** @description ##ENTER## */ + message?: string; + /** @description ##ENTER## */ + success?: boolean; + }; + Search: ( + | components['schemas']['Game'] + | components['schemas']['Franchise'] + | components['schemas']['Character'] + | components['schemas']['Concept'] + | components['schemas']['Object'] + | components['schemas']['Location'] + | components['schemas']['Person'] + | components['schemas']['Company'] + | components['schemas']['Video'] + ) & { + resource_type?: components['schemas']['ResourceType'] & unknown; + }; + /** @description ##ENTER## */ + Sort: string; + /** @description ##ENTER## */ + SubscriberOnly: boolean; + /** @description ##ENTER## */ + Theme: { + /** + * @description URL pointing to the theme detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description For use in single item api call for theme. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the theme. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Name of the theme. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the theme on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'Theme.Detail': components['schemas']['Theme'] & unknown; + /** @description ##ENTER## */ + TimeToSave: number; + /** @description ##ENTER## */ + Type: { + /** + * @description The name of the type's detail resource. + * @example ##WRONG TYPE## + */ + detail_resource_name?: string; + /** + * @description Unique ID of the type. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description The name of the type's list resource. + * @example ##WRONG TYPE## + */ + list_resource_name?: string; + }; + /** @description ##ENTER## */ + UserReview: { + /** + * @description URL pointing to the user_review detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Date the user_review was added to Giant Bomb. + * @example ##WRONG TYPE## + */ + date_added?: string; + /** + * @description Date the user_review was last updated on Giant Bomb. + * @example ##WRONG TYPE## + */ + date_last_updated?: string; + /** + * @description Brief summary of the user_review. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Description of the user_review. + * @example ##WRONG TYPE## + */ + description?: string; + /** + * @description DLC being reviewed. + * @example ##WRONG TYPE## + */ + dlc?: string; + /** + * @description Game being reviewd. + * @example ##WRONG TYPE## + */ + game?: string; + /** + * @description For use in single item api call for user_review. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the user_review. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Release being reviewed. + * @example ##WRONG TYPE## + */ + release?: string; + /** + * @description Name of the review's author. + * @example ##WRONG TYPE## + */ + reviewer?: string; + /** + * @description The score given to the game on a scale of 1 to 5. + * @example ##WRONG TYPE## + */ + score?: string; + /** + * @description URL pointing to the user_review on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'UserReview.Detail': components['schemas']['UserReview'] & unknown; + /** @description ##ENTER## */ + Video: { + /** + * @description URL pointing to the video detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Related objects to the video. + * @example ##WRONG TYPE## + */ + associations?: string; + /** + * @description Brief summary of the video. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description URL for video embed player. To be inserted into an iFrame.
You can add ?autoplay=true to auto-play.
You can add ?time=x where 'x' is an integer between 0 and the length of the video in seconds to start the video at that point.
You can add ?vol=x where 'x' is a decimal between 0 and 1, .75 for example, to set the starting volume.
The above three parameters may be used together. Example: ?time=45&vol=.5&autoplay=true
See http://www.giantbomb.com/api/video-embed-sample/ for more information on using the embed player. + * @example ##WRONG TYPE## + */ + embed_player?: string; + /** + * @description For use in single item api call for video. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description URL to the HD version of the video. + * @example ##WRONG TYPE## + */ + hd_url?: string; + /** + * @description URL to the High Res version of the video. + * @example ##WRONG TYPE## + */ + high_url?: string; + /** + * @description Unique ID of the video. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the video. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Length (in seconds) of the video. + * @example ##WRONG TYPE## + */ + length_seconds?: string; + /** + * @description URL to the Low Res version of the video. + * @example ##WRONG TYPE## + */ + low_url?: string; + /** + * @description Name of the video. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description Premium status of video. + * @example ##WRONG TYPE## + */ + premium?: string; + /** + * @description Date the video was published on Giant Bomb. + * @example ##WRONG TYPE## + */ + publish_date?: string; + /** + * @description The time where the user left off watching this video + * @example ##WRONG TYPE## + */ + saved_time?: string; + /** + * @description URL pointing to the video on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + /** + * @description The video's filename. + * @example ##WRONG TYPE## + */ + url?: string; + /** + * @description Author of the video. + * @example ##WRONG TYPE## + */ + user?: string; + /** + * @description Video categories + * @example ##WRONG TYPE## + */ + video_categories?: string; + /** + * @description Video show + * @example ##WRONG TYPE## + */ + video_show?: string; + /** + * @description Video category + * @example ##WRONG TYPE## + */ + video_type?: string; + /** + * @description Youtube ID for the video. + * @example ##WRONG TYPE## + */ + youtube_id?: string; + }; + 'Video.Detail': components['schemas']['Video'] & { + /** + * @description Crew of the video. + * @example ##WRONG TYPE## + */ + crew?: string; + /** + * @description Hosts of the video. + * @example ##WRONG TYPE## + */ + hosts?: string; + }; + /** @description ##ENTER## */ + VideoCategory: { + /** + * @description URL pointing to the video_category detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Brief summary of the video_category. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Unique ID of the video_category. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the video_category. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description Name of the video_category. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the video_category on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'VideoCategory.Detail': components['schemas']['VideoCategory'] & unknown; + /** @description ##ENTER## */ + VideoId: number; + /** @description ##ENTER## */ + VideoShow: { + /** + * @description Is this show currently active + * @example ##WRONG TYPE## + */ + active?: string; + /** + * @description URL pointing to the video_show detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Endpoint to retrieve the videos attached to this video_show. + * @example ##WRONG TYPE## + */ + api_videos_url?: string; + /** + * @description Brief summary of the video_show. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Should this show be displayed in navigation menus + * @example ##WRONG TYPE## + */ + display_nav?: string; + /** + * @description For use in single item api call for video_show. + * @example ##WRONG TYPE## + */ + guid?: string; + /** + * @description Unique ID of the video_show. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Main image of the video_show. + * @example ##WRONG TYPE## + */ + image?: string; + /** + * @description The latest episode of a video show. Overrides other sorts when used as a sort field. + * @example ##WRONG TYPE## + */ + latest?: string; + /** + * @description Show logo. + * @example ##WRONG TYPE## + */ + logo?: string; + /** + * @description Editor ordering. + * @example ##WRONG TYPE## + */ + position?: string; + /** + * @description Premium status of video_show. + * @example ##WRONG TYPE## + */ + premium?: string; + /** + * @description URL pointing to the video_show on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + /** + * @description Title of the video_show. + * @example ##WRONG TYPE## + */ + title?: string; + }; + 'VideoShow.Detail': components['schemas']['VideoShow'] & unknown; + /** @description ##ENTER## */ + VideoType: { + /** + * @description URL pointing to the video_type detail resource. + * @example ##WRONG TYPE## + */ + api_detail_url?: string; + /** + * @description Brief summary of the video_type. + * @example ##WRONG TYPE## + */ + deck?: string; + /** + * @description Unique ID of the video_type. + * @example ##WRONG TYPE## + */ + id?: string; + /** + * @description Name of the video_type. + * @example ##WRONG TYPE## + */ + name?: string; + /** + * @description URL pointing to the video_type on Giant Bomb. + * @example ##WRONG TYPE## + */ + site_detail_url?: string; + }; + 'VideoType.Detail': components['schemas']['VideoType'] & unknown; + }; + responses: { + /** @description ##ENTER## */ + InvalidAPIKey: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['InvalidAPIKey']; + 'application/jsonp': components['schemas']['InvalidAPIKey']; + 'application/xml': components['schemas']['InvalidAPIKey']; + }; + }; + }; + parameters: { + FieldList: components['schemas']['FieldList']; + /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ + Filter: components['schemas']['Filter']; + /** @description The data format of the response takes either xml, json, or jsonp. */ + Format: components['schemas']['Format']; + /** @description Filter by the ID field on the game resource. */ + Game: components['schemas']['GameId']; + /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ + Limit: components['schemas']['Limit']; + /** @description Return results starting with the object at the offset specified. */ + Offset: components['schemas']['Offset']; + /** @description Page number of search results. */ + Page: components['schemas']['Page']; + /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ + Platforms: components['schemas']['PlatformId']; + /** @description The search string. */ + Query: components['schemas']['Query']; + /** @description List of resources to filter results. This filter can accept multiple arguments, each delimited with a ",". Available options are:
game
franchise
character
concept
object
location
person
company
video */ + Resources: components['schemas']['ResourceType'][]; + /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ + Sort: components['schemas']['Sort']; + /** @description ##ENTER## */ + SubscriberOnly: components['schemas']['SubscriberOnly']; + /** @description The number of seconds into the video the current user is */ + TimeToSave: components['schemas']['TimeToSave']; + /** @description Id of the video */ + VideoId: components['schemas']['VideoId']; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/api/schemas/MALAPI.ts b/api/schemas/MALAPI.ts new file mode 100644 index 00000000..7c047f63 --- /dev/null +++ b/api/schemas/MALAPI.ts @@ -0,0 +1,6761 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/anime/{id}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeFullById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeCharacters']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/staff': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeStaff']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/episodes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeEpisodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/episodes/{episode}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeEpisodeById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/news': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeNews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/forum': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeForum']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeVideos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/videos/episodes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeVideosEpisodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/pictures': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimePictures']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/statistics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeStatistics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/moreinfo': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeMoreInfo']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/recommendations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeRecommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/userupdates': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeUserUpdates']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/relations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeRelations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/themes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeThemes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/external': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeExternal']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime/{id}/streaming': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeStreaming']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterFullById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterAnime']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterManga']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}/voices': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterVoiceActors']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters/{id}/pictures': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharacterPictures']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/clubs/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getClubsById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/clubs/{id}/members': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getClubMembers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/clubs/{id}/staff': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getClubStaff']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/clubs/{id}/relations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getClubRelations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/genres/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeGenres']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/genres/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaGenres']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/magazines': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMagazines']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaFullById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaCharacters']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/news': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaNews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/forum': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaTopics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/pictures': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaPictures']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/statistics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaStatistics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/moreinfo': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaMoreInfo']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/recommendations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaRecommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/userupdates': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaUserUpdates']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/relations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaRelations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga/{id}/external': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaExternal']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonFullById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonAnime']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}/voices': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonVoices']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonManga']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people/{id}/pictures': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPersonPictures']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/producers/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getProducerById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/producers/{id}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getProducerFullById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/producers/{id}/external': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getProducerExternal']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/random/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRandomAnime']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/random/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRandomManga']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/random/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRandomCharacters']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/random/people': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRandomPeople']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/random/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRandomUsers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/recommendations/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRecentAnimeRecommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/recommendations/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRecentMangaRecommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/reviews/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRecentAnimeReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/reviews/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getRecentMangaReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/schedules': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getSchedules']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getAnimeSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getMangaSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/people': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getPeopleSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getCharactersSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUsersSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/userbyid/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/clubs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getClubsSearch']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/producers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getProducers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/seasons/now': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getSeasonNow']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/seasons/{year}/{season}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getSeason']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/seasons': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getSeasonsList']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/seasons/upcoming': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getSeasonUpcoming']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/top/anime': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getTopAnime']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/top/manga': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getTopManga']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/top/people': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getTopPeople']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/top/characters': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getTopCharacters']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/top/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getTopReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/full': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserFullProfile']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserProfile']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/statistics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserStatistics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/favorites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserFavorites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/userupdates': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserUpdates']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/about': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserAbout']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/history': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserHistory']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/friends': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserFriends']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/animelist': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * @deprecated + * @description User Anime lists have been discontinued since May 1st, 2022. Read more + */ + get: operations['getUserAnimelist']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/mangalist': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * @deprecated + * @description User Manga lists have been discontinued since May 1st, 2022. Read more + */ + get: operations['getUserMangaList']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserReviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/recommendations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserRecommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/clubs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserClubs']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/users/{username}/external': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getUserExternal']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/watch/episodes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getWatchRecentEpisodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/watch/episodes/popular': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getWatchPopularEpisodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/watch/promos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getWatchRecentPromos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/watch/promos/popular': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['getWatchPopularPromos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** + * @description User's anime list status filter options + * @enum {string} + */ + user_anime_list_status_filter: 'all' | 'watching' | 'completed' | 'onhold' | 'dropped' | 'plantowatch'; + /** + * @description Available Anime order_by properties + * @enum {string} + */ + anime_search_query_orderby: 'mal_id' | 'title' | 'start_date' | 'end_date' | 'episodes' | 'score' | 'scored_by' | 'rank' | 'popularity' | 'members' | 'favorites'; + /** + * @description Available Anime audience ratings

Ratings
  • G - All Ages
  • PG - Children
  • PG-13 - Teens 13 or older
  • R - 17+ (violence & profanity)
  • R+ - Mild Nudity
  • Rx - Hentai
+ * @enum {string} + */ + anime_search_query_rating: 'g' | 'pg' | 'pg13' | 'r17' | 'r' | 'rx'; + /** + * @description Available Anime statuses + * @enum {string} + */ + anime_search_query_status: 'airing' | 'complete' | 'upcoming'; + /** + * @description Available Anime types + * @enum {string} + */ + anime_search_query_type: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music' | 'cm' | 'pv' | 'tv_special'; + /** + * @description Available Character order_by properties + * @enum {string} + */ + characters_search_query_orderby: 'mal_id' | 'name' | 'favorites'; + /** + * @description Club Search Query Category + * @enum {string} + */ + club_search_query_category: + | 'anime' + | 'manga' + | 'actors_and_artists' + | 'characters' + | 'cities_and_neighborhoods' + | 'companies' + | 'conventions' + | 'games' + | 'japan' + | 'music' + | 'other' + | 'schools'; + /** + * @description Club Search Query OrderBy + * @enum {string} + */ + club_search_query_orderby: 'mal_id' | 'name' | 'members_count' | 'created'; + /** + * @description Club Search Query Type + * @enum {string} + */ + club_search_query_type: 'public' | 'private' | 'secret'; + /** + * @description Users Search Query Gender. + * @enum {string} + */ + users_search_query_gender: 'any' | 'male' | 'female' | 'nonbinary'; + /** + * @description Filter genres by type + * @enum {string} + */ + genre_query_filter: 'genres' | 'explicit_genres' | 'themes' | 'demographics'; + /** + * @description Order by magazine data + * @enum {string} + */ + magazines_query_orderby: 'mal_id' | 'name' | 'count'; + /** + * @description User's anime list status filter options + * @enum {string} + */ + user_manga_list_status_filter: 'all' | 'reading' | 'completed' | 'onhold' | 'dropped' | 'plantoread'; + /** + * @description Available Manga order_by properties + * @enum {string} + */ + manga_search_query_orderby: + | 'mal_id' + | 'title' + | 'start_date' + | 'end_date' + | 'chapters' + | 'volumes' + | 'score' + | 'scored_by' + | 'rank' + | 'popularity' + | 'members' + | 'favorites'; + /** + * @description Available Manga statuses + * @enum {string} + */ + manga_search_query_status: 'publishing' | 'complete' | 'hiatus' | 'discontinued' | 'upcoming'; + /** + * @description Available Manga types + * @enum {string} + */ + manga_search_query_type: 'manga' | 'novel' | 'lightnovel' | 'oneshot' | 'doujin' | 'manhwa' | 'manhua'; + /** + * @description Available People order_by properties + * @enum {string} + */ + people_search_query_orderby: 'mal_id' | 'name' | 'birthday' | 'favorites'; + /** + * @description Producers Search Query Order By + * @enum {string} + */ + producers_query_orderby: 'mal_id' | 'count' | 'favorites' | 'established'; + /** + * @description Search query sort direction + * @enum {string} + */ + search_query_sort: 'desc' | 'asc'; + /** + * @description Top items filter types + * @enum {string} + */ + top_anime_filter: 'airing' | 'upcoming' | 'bypopularity' | 'favorite'; + /** + * @description Top items filter types + * @enum {string} + */ + top_manga_filter: 'publishing' | 'upcoming' | 'bypopularity' | 'favorite'; + /** + * @description The type of reviews to filter by. Defaults to anime. + * @enum {string} + */ + top_reviews_type_enum: 'anime' | 'manga'; + /** @description Anime Episodes Resource */ + anime_episodes: { + data?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL. This is the URL of the episode's video. If there is no video url, this will be null. */ + url?: string | null; + /** @description Title */ + title?: string; + /** @description Title Japanese */ + title_japanese?: string | null; + /** @description title_romanji */ + title_romanji?: string | null; + /** @description Aired Date ISO8601 */ + aired?: string | null; + /** @description Aggregated episode score (1.00 - 5.00) based on MyAnimeList user voting */ + score?: Record | null; + /** @description Filler episode */ + filler?: boolean; + /** @description Recap episode */ + recap?: boolean; + /** @description Episode discussion forum URL */ + forum_url?: string | null; + }[]; + } & components['schemas']['pagination']; + /** @description Anime News Resource */ + anime_news: components['schemas']['pagination'] & components['schemas']['news']; + /** @description Anime Videos Episodes Resource */ + anime_videos_episodes: { + data?: { + /** @description MyAnimeList ID or Episode Number */ + mal_id?: number; + /** @description Episode Title */ + title?: string; + /** @description Episode Subtitle */ + episode?: string; + /** @description Episode Page URL */ + url?: string; + images?: components['schemas']['common_images']; + }[]; + } & components['schemas']['pagination']; + /** @description Character Pictures */ + character_pictures: { + data?: { + /** @description Default JPG Image Size URL */ + image_url?: string | null; + /** @description Large JPG Image Size URL */ + large_image_url?: string | null; + }[]; + }; + /** @description Club Member */ + club_member: { + data?: { + /** @description User's username */ + username?: string; + /** @description User URL */ + url?: string; + images?: components['schemas']['user_images']; + }[]; + }; + /** @description Manga News Resource */ + manga_news: components['schemas']['pagination'] & components['schemas']['news']; + /** @description Manga Pictures */ + manga_pictures: { + data?: components['schemas']['manga_images'][]; + }; + /** @description Character Pictures */ + person_pictures: { + data?: components['schemas']['people_images'][]; + }; + /** @description Random Resources */ + random: { + data?: (components['schemas']['anime'] | components['schemas']['manga'] | components['schemas']['character'] | components['schemas']['person'])[]; + }; + /** @description Anime resources currently airing */ + schedules: { + data?: components['schemas']['anime'][]; + } & components['schemas']['pagination_plus']; + /** @description User Results */ + users_search: { + data?: { + /** @description MyAnimeList URL */ + url?: string; + /** @description MyAnimeList Username */ + username?: string; + images?: components['schemas']['user_images']; + /** @description Last Online Date ISO8601 */ + last_online?: string; + }[]; + } & components['schemas']['pagination']; + /** @description List of available seasons */ + seasons: { + data?: { + /** @description Year */ + year?: number; + /** @description List of available seasons */ + seasons?: string[]; + }[]; + }; + /** @description Anime & Manga Reviews Resource */ + reviews_collection: { + data?: (components['schemas']['anime_review'] | components['schemas']['manga_review'])[]; + }; + /** @description User Friends */ + user_friends: { + data?: ({ + user?: components['schemas']['user_meta']; + } & { + /** @description Last Online Date ISO8601 format */ + last_online?: string; + /** @description Friends Since Date ISO8601 format */ + friends_since?: string; + })[]; + } & components['schemas']['pagination']; + /** @description User Clubs */ + user_clubs: { + data?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Club Name */ + name?: string; + /** @description Club URL */ + url?: string; + }[]; + } & components['schemas']['pagination']; + /** @description Watch Episodes */ + watch_episodes: { + data?: { + entry?: components['schemas']['anime_meta']; + /** @description Recent Episodes (max 2 listed) */ + episodes?: { + /** @description MyAnimeList ID */ + mal_id?: string; + /** @description MyAnimeList URL */ + url?: string; + /** @description Episode Title */ + title?: string; + /** @description For MyAnimeList Premium Users */ + premium?: boolean; + }[]; + /** @description Region Locked Episode */ + region_locked?: boolean; + }[]; + } & components['schemas']['pagination']; + /** @description Watch Promos */ + watch_promos: components['schemas']['pagination'] & { + data?: { + /** @description Promo Title */ + title?: string; + entry?: components['schemas']['anime_meta']; + trailer?: components['schemas']['trailer']; + }[]; + }; + /** @description Anime Characters Resource */ + anime_characters: { + data?: { + /** @description Character details */ + character?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['character_images']; + /** @description Character Name */ + name?: string; + }; + /** @description Character's Role */ + role?: string; + voice_actors?: { + person?: { + mal_id?: number; + url?: string; + images?: components['schemas']['people_images']; + name?: string; + }; + language?: string; + }[]; + }[]; + }; + /** @description Anime Collection Resource */ + anime_search: { + data?: components['schemas']['anime'][]; + } & components['schemas']['pagination_plus']; + /** @description Anime Episode Resource */ + anime_episode: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Title */ + title?: string; + /** @description Title Japanese */ + title_japanese?: string | null; + /** @description title_romanji */ + title_romanji?: string | null; + /** @description Episode duration in seconds */ + duration?: number | null; + /** @description Aired Date ISO8601 */ + aired?: string | null; + /** @description Filler episode */ + filler?: boolean; + /** @description Recap episode */ + recap?: boolean; + /** @description Episode Synopsis */ + synopsis?: string | null; + }; + /** @description Full anime Resource */ + anime_full: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['anime_images']; + trailer?: components['schemas']['trailer_base']; + /** @description Whether the entry is pending approval on MAL or not */ + approved?: boolean; + /** @description All titles */ + titles?: components['schemas']['title'][]; + /** + * @deprecated + * @description Title + */ + title?: string; + /** + * @deprecated + * @description English Title + */ + title_english?: string | null; + /** + * @deprecated + * @description Japanese Title + */ + title_japanese?: string | null; + /** + * @deprecated + * @description Other Titles + */ + title_synonyms?: string[]; + /** + * @description Anime Type + * @enum {string|null} + */ + type?: 'TV' | 'OVA' | 'Movie' | 'Special' | 'ONA' | 'Music' | null; + /** @description Original Material/Source adapted from */ + source?: string | null; + /** @description Episode count */ + episodes?: number | null; + /** + * @description Airing status + * @enum {string|null} + */ + status?: 'Finished Airing' | 'Currently Airing' | 'Not yet aired' | null; + /** @description Airing boolean */ + airing?: boolean; + aired?: components['schemas']['daterange']; + /** @description Parsed raw duration */ + duration?: string | null; + /** + * @description Anime audience rating + * @enum {string|null} + */ + rating?: 'G - All Ages' | 'PG - Children' | 'PG-13 - Teens 13 or older' | 'R - 17+ (violence & profanity)' | 'R+ - Mild Nudity' | 'Rx - Hentai' | null; + /** + * Format: float + * @description Score + */ + score?: number | null; + /** @description Number of users */ + scored_by?: number | null; + /** @description Ranking */ + rank?: number | null; + /** @description Popularity */ + popularity?: number | null; + /** @description Number of users who have added this entry to their list */ + members?: number | null; + /** @description Number of users who have favorited this entry */ + favorites?: number | null; + /** @description Synopsis */ + synopsis?: string | null; + /** @description Background */ + background?: string | null; + /** + * @description Season + * @enum {string|null} + */ + season?: 'summer' | 'winter' | 'spring' | 'fall' | null; + /** @description Year */ + year?: number | null; + broadcast?: components['schemas']['broadcast']; + producers?: components['schemas']['mal_url'][]; + licensors?: components['schemas']['mal_url'][]; + studios?: components['schemas']['mal_url'][]; + genres?: components['schemas']['mal_url'][]; + explicit_genres?: components['schemas']['mal_url'][]; + themes?: components['schemas']['mal_url'][]; + demographics?: components['schemas']['mal_url'][]; + relations?: { + /** @description Relation type */ + relation?: string; + entry?: components['schemas']['mal_url'][]; + }[]; + theme?: { + openings?: string[]; + endings?: string[]; + }; + external?: { + name?: string; + url?: string; + }[]; + streaming?: { + name?: string; + url?: string; + }[]; + }; + /** @description Anime Relations */ + anime_relations: { + data?: { + /** @description Relation type */ + relation?: string; + entry?: components['schemas']['mal_url'][]; + }[]; + }; + /** @description Anime Resource */ + anime: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['anime_images']; + trailer?: components['schemas']['trailer_base']; + /** @description Whether the entry is pending approval on MAL or not */ + approved?: boolean; + /** @description All titles */ + titles?: components['schemas']['title'][]; + /** + * @deprecated + * @description Title + */ + title?: string; + /** + * @deprecated + * @description English Title + */ + title_english?: string | null; + /** + * @deprecated + * @description Japanese Title + */ + title_japanese?: string | null; + /** + * @deprecated + * @description Other Titles + */ + title_synonyms?: string[]; + /** + * @description Anime Type + * @enum {string|null} + */ + type?: 'TV' | 'OVA' | 'Movie' | 'Special' | 'ONA' | 'Music' | null; + /** @description Original Material/Source adapted from */ + source?: string | null; + /** @description Episode count */ + episodes?: number | null; + /** + * @description Airing status + * @enum {string|null} + */ + status?: 'Finished Airing' | 'Currently Airing' | 'Not yet aired' | null; + /** @description Airing boolean */ + airing?: boolean; + aired?: components['schemas']['daterange']; + /** @description Parsed raw duration */ + duration?: string | null; + /** + * @description Anime audience rating + * @enum {string|null} + */ + rating?: 'G - All Ages' | 'PG - Children' | 'PG-13 - Teens 13 or older' | 'R - 17+ (violence & profanity)' | 'R+ - Mild Nudity' | 'Rx - Hentai' | null; + /** + * Format: float + * @description Score + */ + score?: number | null; + /** @description Number of users */ + scored_by?: number | null; + /** @description Ranking */ + rank?: number | null; + /** @description Popularity */ + popularity?: number | null; + /** @description Number of users who have added this entry to their list */ + members?: number | null; + /** @description Number of users who have favorited this entry */ + favorites?: number | null; + /** @description Synopsis */ + synopsis?: string | null; + /** @description Background */ + background?: string | null; + /** + * @description Season + * @enum {string|null} + */ + season?: 'summer' | 'winter' | 'spring' | 'fall' | null; + /** @description Year */ + year?: number | null; + broadcast?: components['schemas']['broadcast']; + producers?: components['schemas']['mal_url'][]; + licensors?: components['schemas']['mal_url'][]; + studios?: components['schemas']['mal_url'][]; + genres?: components['schemas']['mal_url'][]; + explicit_genres?: components['schemas']['mal_url'][]; + themes?: components['schemas']['mal_url'][]; + demographics?: components['schemas']['mal_url'][]; + }; + /** @description Anime Staff Resource */ + anime_staff: { + data?: { + /** @description Person details */ + person?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['people_images']; + /** @description Name */ + name?: string; + }; + /** @description Staff Positions */ + positions?: string[]; + }[]; + }; + /** @description Anime Statistics Resource */ + anime_statistics: { + data?: { + /** @description Number of users watching the resource */ + watching?: number; + /** @description Number of users who have completed the resource */ + completed?: number; + /** @description Number of users who have put the resource on hold */ + on_hold?: number; + /** @description Number of users who have dropped the resource */ + dropped?: number; + /** @description Number of users who have planned to watch the resource */ + plan_to_watch?: number; + /** @description Total number of users who have the resource added to their lists */ + total?: number; + scores?: { + /** @description Scoring value */ + score?: number; + /** @description Number of votes for this score */ + votes?: number; + /** + * Format: float + * @description Percentage of votes for this score + */ + percentage?: number; + }[]; + }; + }; + /** @description Anime Opening and Ending Themes */ + anime_themes: { + data?: { + openings?: string[]; + endings?: string[]; + }; + }; + /** @description Anime Videos Resource */ + anime_videos: { + data?: { + promo?: { + /** @description Title */ + title?: string; + trailer?: components['schemas']['trailer']; + }[]; + episodes?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Title */ + title?: string; + /** @description Episode */ + episode?: string; + images?: components['schemas']['common_images']; + }[]; + music_videos?: { + /** @description Title */ + title?: string; + video?: components['schemas']['trailer']; + meta?: { + title?: string | null; + author?: string | null; + }; + }[]; + }; + }; + /** @description Character casted in anime */ + character_anime: { + data?: { + /** @description Character's Role */ + role?: string; + anime?: components['schemas']['anime_meta']; + }[]; + }; + /** @description Characters Search Resource */ + characters_search: { + data?: components['schemas']['character'][]; + } & components['schemas']['pagination_plus']; + /** @description Character Resource */ + character_full: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['character_images']; + /** @description Name */ + name?: string; + /** @description Name */ + name_kanji?: string | null; + /** @description Other Names */ + nicknames?: string[]; + /** @description Number of users who have favorited this entry */ + favorites?: number; + /** @description Biography */ + about?: string | null; + anime?: { + /** @description Character's Role */ + role?: string; + anime?: components['schemas']['anime_meta']; + }[]; + manga?: { + /** @description Character's Role */ + role?: string; + manga?: components['schemas']['manga_meta']; + }[]; + voices?: { + /** @description Character's Role */ + language?: string; + person?: components['schemas']['person_meta']; + }[]; + }; + /** @description Character casted in manga */ + character_manga: { + data?: { + /** @description Character's Role */ + role?: string; + manga?: components['schemas']['manga_meta']; + }[]; + }; + /** @description Character Resource */ + character: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['character_images']; + /** @description Name */ + name?: string; + /** @description Name */ + name_kanji?: string | null; + /** @description Other Names */ + nicknames?: string[]; + /** @description Number of users who have favorited this entry */ + favorites?: number; + /** @description Biography */ + about?: string | null; + }; + /** @description Character voice actors */ + character_voice_actors: { + data?: { + /** @description Character's Role */ + language?: string; + person?: components['schemas']['person_meta']; + }[]; + }; + /** @description Clubs Search Resource */ + clubs_search: { + data?: components['schemas']['club'][]; + } & components['schemas']['pagination']; + /** @description Club Relations */ + club_relations: { + data?: { + anime?: components['schemas']['mal_url'][]; + manga?: components['schemas']['mal_url'][]; + characters?: components['schemas']['mal_url'][]; + }; + }; + /** @description Club Resource */ + club: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Club name */ + name?: string; + /** @description Club URL */ + url?: string; + images?: components['schemas']['common_images']; + /** @description Number of club members */ + members?: number; + /** + * @description Club Category + * @enum {string} + */ + category?: + | 'actors & artists' + | 'anime' + | 'characters' + | 'cities & neighborhoods' + | 'companies' + | 'conventions' + | 'games' + | 'japan' + | 'manga' + | 'music' + | 'others' + | 'schools'; + /** @description Date Created ISO8601 */ + created?: string; + /** + * @description Club access + * @enum {string} + */ + access?: 'public' | 'private' | 'secret'; + }; + /** @description Club Staff Resource */ + club_staff: { + data?: { + /** @description User URL */ + url?: string; + /** @description User's username */ + username?: string; + }[]; + }; + /** @description Youtube Details */ + trailer: components['schemas']['trailer_base'] & components['schemas']['trailer_images']; + /** @description Youtube Details */ + trailer_base: { + /** @description YouTube ID */ + youtube_id?: string | null; + /** @description YouTube URL */ + url?: string | null; + /** @description Parsed Embed URL */ + embed_url?: string | null; + }; + /** @description Youtube Images */ + trailer_images: { + images?: { + /** @description Default Image Size URL (120x90) */ + image_url?: string | null; + /** @description Small Image Size URL (640x480) */ + small_image_url?: string | null; + /** @description Medium Image Size URL (320x180) */ + medium_image_url?: string | null; + /** @description Large Image Size URL (480x360) */ + large_image_url?: string | null; + /** @description Maximum Image Size URL (1280x720) */ + maximum_image_url?: string | null; + }; + }; + /** @description Date range */ + daterange: { + /** @description Date ISO8601 */ + from?: string | null; + /** @description Date ISO8601 */ + to?: string | null; + /** @description Date Prop */ + prop?: { + /** @description Date Prop From */ + from?: { + /** @description Day */ + day?: number | null; + /** @description Month */ + month?: number | null; + /** @description Year */ + year?: number | null; + }; + /** @description Date Prop To */ + to?: { + /** @description Day */ + day?: number | null; + /** @description Month */ + month?: number | null; + /** @description Year */ + year?: number | null; + }; + /** @description Raw parsed string */ + string?: string | null; + }; + }; + /** @description Broadcast Details */ + broadcast: { + /** @description Day of the week */ + day?: string | null; + /** @description Time in 24 hour format */ + time?: string | null; + /** @description Timezone (Tz Database format https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) */ + timezone?: string | null; + /** @description Raw parsed broadcast string */ + string?: string | null; + }; + /** @description Parsed URL Data */ + mal_url: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Type of resource */ + type?: string; + /** @description Resource Name/Title */ + name?: string; + /** @description MyAnimeList URL */ + url?: string; + }; + /** @description Parsed URL Data */ + mal_url_2: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Type of resource */ + type?: string; + /** @description Resource Name/Title */ + title?: string; + /** @description MyAnimeList URL */ + url?: string; + }; + /** @description Entry Meta data */ + entry_meta: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Image URL */ + image_url?: string; + /** @description Entry Name/Title */ + name?: string; + }; + /** @description Related resources */ + relation: { + /** @description Relation type */ + relation?: string; + /** @description Related entries */ + entry?: components['schemas']['mal_url'][]; + }; + pagination: { + pagination?: { + last_visible_page?: number; + has_next_page?: boolean; + }; + }; + pagination_plus: { + pagination?: { + last_visible_page?: number; + has_next_page?: boolean; + current_page?: number; + items?: { + count?: number; + total?: number; + per_page?: number; + }; + }; + }; + user_meta: { + /** @description MyAnimeList Username */ + username?: string; + /** @description MyAnimeList Profile URL */ + url?: string; + images?: components['schemas']['user_images']; + }; + /** @description User Meta By ID */ + user_by_id: { + /** @description MyAnimeList URL */ + url?: string; + /** @description MyAnimeList Username */ + username?: string; + }; + user_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + }; + /** @description Available images in WEBP */ + webp?: { + /** @description Image URL WEBP */ + image_url?: string | null; + }; + }; + anime_meta: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['anime_images']; + /** @description Entry title */ + title?: string; + }; + manga_meta: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['manga_images']; + /** @description Entry title */ + title?: string; + }; + character_meta: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['character_images']; + /** @description Entry name */ + name?: string; + }; + person_meta: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['people_images']; + /** @description Entry name */ + name?: string; + }; + anime_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + /** @description Small Image URL JPG */ + small_image_url?: string | null; + /** @description Image URL JPG */ + large_image_url?: string | null; + }; + /** @description Available images in WEBP */ + webp?: { + /** @description Image URL WEBP */ + image_url?: string | null; + /** @description Small Image URL WEBP */ + small_image_url?: string | null; + /** @description Image URL WEBP */ + large_image_url?: string | null; + }; + }; + manga_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + /** @description Small Image URL JPG */ + small_image_url?: string | null; + /** @description Image URL JPG */ + large_image_url?: string | null; + }; + /** @description Available images in WEBP */ + webp?: { + /** @description Image URL WEBP */ + image_url?: string | null; + /** @description Small Image URL WEBP */ + small_image_url?: string | null; + /** @description Image URL WEBP */ + large_image_url?: string | null; + }; + }; + character_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + /** @description Small Image URL JPG */ + small_image_url?: string | null; + }; + /** @description Available images in WEBP */ + webp?: { + /** @description Image URL WEBP */ + image_url?: string | null; + /** @description Small Image URL WEBP */ + small_image_url?: string | null; + }; + }; + people_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + }; + }; + common_images: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG */ + image_url?: string | null; + }; + }; + title: { + /** @description Title type */ + type?: string; + /** @description Title value */ + title?: string; + }; + /** @description External links */ + external_links: { + data?: { + name?: string; + url?: string; + }[]; + }; + /** @description Forum Resource */ + forum: { + data?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Title */ + title?: string; + /** @description Post Date ISO8601 */ + date?: string; + /** @description Author MyAnimeList Username */ + author_username?: string; + /** @description Author Profile URL */ + author_url?: string; + /** @description Comment count */ + comments?: number; + /** @description Last comment details */ + last_comment?: { + /** @description Last comment URL */ + url?: string; + /** @description Author MyAnimeList Username */ + author_username?: string; + /** @description Author Profile URL */ + author_url?: string; + /** @description Last comment date posted ISO8601 */ + date?: string | null; + }; + }[]; + }; + /** @description Genres Collection Resource */ + genres: { + data?: components['schemas']['genre'][]; + }; + /** @description Genre Resource */ + genre: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Genre Name */ + name?: string; + /** @description MyAnimeList URL */ + url?: string; + /** @description Genre's entry count */ + count?: number; + }; + /** @description Magazine Collection Resource */ + magazines: { + data?: components['schemas']['magazine'][]; + } & components['schemas']['pagination']; + /** @description Magazine Resource */ + magazine: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description Magazine Name */ + name?: string; + /** @description MyAnimeList URL */ + url?: string; + /** @description Magazine's manga count */ + count?: number; + }; + /** @description Manga Characters Resource */ + manga_characters: { + data?: { + character?: components['schemas']['character_meta']; + /** @description Character's Role */ + role?: string; + }[]; + }; + /** @description Manga Search Resource */ + manga_search: { + data?: components['schemas']['manga'][]; + } & components['schemas']['pagination_plus']; + /** @description Manga Resource */ + manga_full: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['manga_images']; + /** @description Whether the entry is pending approval on MAL or not */ + approved?: boolean; + /** @description All Titles */ + titles?: components['schemas']['title'][]; + /** + * @deprecated + * @description Title + */ + title?: string; + /** + * @deprecated + * @description English Title + */ + title_english?: string | null; + /** + * @deprecated + * @description Japanese Title + */ + title_japanese?: string | null; + /** + * @deprecated + * @description Other Titles + */ + title_synonyms?: string[]; + /** + * @description Manga Type + * @enum {string|null} + */ + type?: 'Manga' | 'Novel' | 'Light Novel' | 'One-shot' | 'Doujinshi' | 'Manhua' | 'Manhwa' | 'OEL' | null; + /** @description Chapter count */ + chapters?: number | null; + /** @description Volume count */ + volumes?: number | null; + /** + * @description Publishing status + * @enum {string} + */ + status?: 'Finished' | 'Publishing' | 'On Hiatus' | 'Discontinued' | 'Not yet published'; + /** @description Publishing boolean */ + publishing?: boolean; + published?: components['schemas']['daterange']; + /** + * Format: float + * @description Score + */ + score?: number | null; + /** @description Number of users */ + scored_by?: number | null; + /** @description Ranking */ + rank?: number | null; + /** @description Popularity */ + popularity?: number | null; + /** @description Number of users who have added this entry to their list */ + members?: number | null; + /** @description Number of users who have favorited this entry */ + favorites?: number | null; + /** @description Synopsis */ + synopsis?: string | null; + /** @description Background */ + background?: string | null; + authors?: components['schemas']['mal_url'][]; + serializations?: components['schemas']['mal_url'][]; + genres?: components['schemas']['mal_url'][]; + explicit_genres?: components['schemas']['mal_url'][]; + themes?: components['schemas']['mal_url'][]; + demographics?: components['schemas']['mal_url'][]; + relations?: { + /** @description Relation type */ + relation?: string; + entry?: components['schemas']['mal_url'][]; + }[]; + external?: { + name?: string; + url?: string; + }[]; + }; + /** @description Manga Resource */ + manga: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['manga_images']; + /** @description Whether the entry is pending approval on MAL or not */ + approved?: boolean; + /** @description All Titles */ + titles?: components['schemas']['title'][]; + /** + * @deprecated + * @description Title + */ + title?: string; + /** + * @deprecated + * @description English Title + */ + title_english?: string | null; + /** + * @deprecated + * @description Japanese Title + */ + title_japanese?: string | null; + /** + * @description Manga Type + * @enum {string|null} + */ + type?: 'Manga' | 'Novel' | 'Light Novel' | 'One-shot' | 'Doujinshi' | 'Manhua' | 'Manhwa' | 'OEL' | null; + /** @description Chapter count */ + chapters?: number | null; + /** @description Volume count */ + volumes?: number | null; + /** + * @description Publishing status + * @enum {string} + */ + status?: 'Finished' | 'Publishing' | 'On Hiatus' | 'Discontinued' | 'Not yet published'; + /** @description Publishing boolean */ + publishing?: boolean; + published?: components['schemas']['daterange']; + /** + * Format: float + * @description Score + */ + score?: number | null; + /** @description Number of users */ + scored_by?: number | null; + /** @description Ranking */ + rank?: number | null; + /** @description Popularity */ + popularity?: number | null; + /** @description Number of users who have added this entry to their list */ + members?: number | null; + /** @description Number of users who have favorited this entry */ + favorites?: number | null; + /** @description Synopsis */ + synopsis?: string | null; + /** @description Background */ + background?: string | null; + authors?: components['schemas']['mal_url'][]; + serializations?: components['schemas']['mal_url'][]; + genres?: components['schemas']['mal_url'][]; + explicit_genres?: components['schemas']['mal_url'][]; + themes?: components['schemas']['mal_url'][]; + demographics?: components['schemas']['mal_url'][]; + }; + /** @description Manga Statistics Resource */ + manga_statistics: { + data?: { + /** @description Number of users reading the resource */ + reading?: number; + /** @description Number of users who have completed the resource */ + completed?: number; + /** @description Number of users who have put the resource on hold */ + on_hold?: number; + /** @description Number of users who have dropped the resource */ + dropped?: number; + /** @description Number of users who have planned to read the resource */ + plan_to_read?: number; + /** @description Total number of users who have the resource added to their lists */ + total?: number; + scores?: { + /** @description Scoring value */ + score?: number; + /** @description Number of votes for this score */ + votes?: number; + /** + * Format: float + * @description Percentage of votes for this score + */ + percentage?: number; + }[]; + }; + }; + /** @description More Info Resource */ + moreinfo: { + data?: { + /** @description Additional information on the entry */ + moreinfo?: string | null; + }; + }; + news: { + data?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Title */ + title?: string; + /** @description Post Date ISO8601 */ + date?: string; + /** @description Author MyAnimeList Username */ + author_username?: string; + /** @description Author Profile URL */ + author_url?: string; + /** @description Forum topic URL */ + forum_url?: string; + images?: components['schemas']['common_images']; + /** @description Comment count */ + comments?: number; + /** @description Excerpt */ + excerpt?: string; + }[]; + }; + /** @description Person anime staff positions */ + person_anime: { + data?: { + /** @description Person's position */ + position?: string; + anime?: components['schemas']['anime_meta']; + }[]; + }; + /** @description People Search */ + people_search: { + data?: components['schemas']['person'][]; + } & components['schemas']['pagination_plus']; + /** @description Person Resource */ + person_full: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Person's website URL */ + website_url?: string | null; + images?: components['schemas']['people_images']; + /** @description Name */ + name?: string; + /** @description Given Name */ + given_name?: string | null; + /** @description Family Name */ + family_name?: string | null; + /** @description Other Names */ + alternate_names?: string[]; + /** @description Birthday Date ISO8601 */ + birthday?: string | null; + /** @description Number of users who have favorited this entry */ + favorites?: number; + /** @description Biography */ + about?: string | null; + anime?: { + /** @description Person's position */ + position?: string; + anime?: components['schemas']['anime_meta']; + }[]; + manga?: { + /** @description Person's position */ + position?: string; + manga?: components['schemas']['manga_meta']; + }[]; + voices?: { + /** @description Person's Character's role in the anime */ + role?: string; + anime?: components['schemas']['anime_meta']; + character?: components['schemas']['character_meta']; + }[]; + }; + /** @description Person's mangaography */ + person_manga: { + data?: { + /** @description Person's position */ + position?: string; + manga?: components['schemas']['manga_meta']; + }[]; + }; + /** @description Person Resource */ + person: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description Person's website URL */ + website_url?: string | null; + images?: components['schemas']['people_images']; + /** @description Name */ + name?: string; + /** @description Given Name */ + given_name?: string | null; + /** @description Family Name */ + family_name?: string | null; + /** @description Other Names */ + alternate_names?: string[]; + /** @description Birthday Date ISO8601 */ + birthday?: string | null; + /** @description Number of users who have favorited this entry */ + favorites?: number; + /** @description Biography */ + about?: string | null; + }; + /** @description Person's voice acting roles */ + person_voice_acting_roles: { + data?: { + /** @description Person's Character's role in the anime */ + role?: string; + anime?: components['schemas']['anime_meta']; + character?: components['schemas']['character_meta']; + }[]; + }; + /** @description Pictures Resource */ + pictures: { + data?: { + images?: components['schemas']['anime_images']; + }[]; + }; + /** @description Pictures Resource */ + pictures_variants: { + data?: { + images?: components['schemas']['common_images']; + }[]; + }; + /** @description Producers Collection Resource */ + producers: { + data?: components['schemas']['producer'][]; + } & components['schemas']['pagination']; + /** @description Producers Resource */ + producer_full: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description All titles */ + titles?: components['schemas']['title'][]; + images?: components['schemas']['common_images']; + /** @description Producers's member favorites count */ + favorites?: number; + /** @description Producers's anime count */ + count?: number; + /** @description Established Date ISO8601 */ + established?: string | null; + /** @description About the Producer */ + about?: string | null; + external?: { + name?: string; + url?: string; + }[]; + }; + /** @description Producers Resource */ + producer: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList URL */ + url?: string; + /** @description All titles */ + titles?: components['schemas']['title'][]; + images?: components['schemas']['common_images']; + /** @description Producers's member favorites count */ + favorites?: number; + /** @description Producers's anime count */ + count?: number; + /** @description Established Date ISO8601 */ + established?: string | null; + /** @description About the Producer */ + about?: string | null; + }; + user_about: { + data?: { + /** @description User About. NOTE: About information is customizable by users through BBCode on MyAnimeList. This means users can add multimedia content, different text sizes, etc. Due to this freeform, Jikan returns parsed HTML. Validate on your end! */ + about?: string | null; + }[]; + }; + user_favorites: { + /** @description Favorite Anime */ + anime?: ({ + type?: string; + start_year?: number; + } & components['schemas']['anime_meta'])[]; + /** @description Favorite Manga */ + manga?: ({ + type?: string; + start_year?: number; + } & components['schemas']['manga_meta'])[]; + /** @description Favorite Characters */ + characters?: (components['schemas']['character_meta'] & components['schemas']['mal_url_2'])[]; + /** @description Favorite People */ + people?: components['schemas']['character_meta'][]; + }; + /** @description Transform the resource into an array. */ + user_profile_full: { + /** @description MyAnimeList ID */ + mal_id?: number | null; + /** @description MyAnimeList Username */ + username?: string; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['user_images']; + /** @description Last Online Date ISO8601 */ + last_online?: string | null; + /** @description User Gender */ + gender?: string | null; + /** @description Birthday Date ISO8601 */ + birthday?: string | null; + /** @description Location */ + location?: string | null; + /** @description Joined Date ISO8601 */ + joined?: string | null; + statistics?: { + /** @description Anime Statistics */ + anime?: { + /** + * Format: float + * @description Number of days spent watching Anime + */ + days_watched?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Anime Watching */ + watching?: number; + /** @description Anime Completed */ + completed?: number; + /** @description Anime On-Hold */ + on_hold?: number; + /** @description Anime Dropped */ + dropped?: number; + /** @description Anime Planned to Watch */ + plan_to_watch?: number; + /** @description Total Anime entries on User list */ + total_entries?: number; + /** @description Anime re-watched */ + rewatched?: number; + /** @description Number of Anime Episodes Watched */ + episodes_watched?: number; + }; + /** @description Manga Statistics */ + manga?: { + /** + * Format: float + * @description Number of days spent reading Manga + */ + days_read?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Manga Reading */ + reading?: number; + /** @description Manga Completed */ + completed?: number; + /** @description Manga On-Hold */ + on_hold?: number; + /** @description Manga Dropped */ + dropped?: number; + /** @description Manga Planned to Read */ + plan_to_read?: number; + /** @description Total Manga entries on User list */ + total_entries?: number; + /** @description Manga re-read */ + reread?: number; + /** @description Number of Manga Chapters Read */ + chapters_read?: number; + /** @description Number of Manga Volumes Read */ + volumes_read?: number; + }; + }; + external?: { + name?: string; + url?: string; + }[]; + }; + user_history: { + data?: components['schemas']['history'][]; + }; + /** @description Transform the resource into an array. */ + history: { + entry?: components['schemas']['mal_url']; + /** @description Number of episodes/chapters watched/read */ + increment?: number; + /** @description Date ISO8601 */ + date?: string; + }; + user_updates: { + data?: { + /** @description Last updated Anime */ + anime?: ({ + entry?: components['schemas']['anime_meta']; + } & { + score?: number | null; + status?: string; + episodes_seen?: number | null; + episodes_total?: number | null; + /** @description ISO8601 format */ + date?: string; + })[]; + /** @description Last updated Manga */ + manga?: ({ + entry?: components['schemas']['manga_meta']; + } & { + score?: number | null; + status?: string; + chapters_read?: number | null; + chapters_total?: number | null; + volumes_read?: number | null; + volumes_total?: number | null; + /** @description ISO8601 format */ + date?: string; + })[]; + }; + }; + user_profile: { + /** @description MyAnimeList ID */ + mal_id?: number | null; + /** @description MyAnimeList Username */ + username?: string; + /** @description MyAnimeList URL */ + url?: string; + images?: components['schemas']['user_images']; + /** @description Last Online Date ISO8601 */ + last_online?: string | null; + /** @description User Gender */ + gender?: string | null; + /** @description Birthday Date ISO8601 */ + birthday?: string | null; + /** @description Location */ + location?: string | null; + /** @description Joined Date ISO8601 */ + joined?: string | null; + }; + /** @description Transform the resource into an array. */ + users_temp: { + data?: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList Username */ + username?: string; + /** @description MyAnimeList URL */ + url?: string; + /** @description Images */ + images?: { + /** @description Available images in JPG */ + jpg?: { + /** @description Image URL JPG (225x335) */ + image_url?: string; + }; + /** @description Available images in WEBP */ + webp?: { + /** @description Image URL WEBP (225x335) */ + image_url?: string; + }; + }; + /** @description Last Online Date ISO8601 */ + last_online?: string; + /** @description User Gender */ + gender?: string; + /** @description Birthday Date ISO8601 */ + birthday?: string; + /** @description Location */ + location?: string; + /** @description Joined Date ISO8601 */ + joined?: string; + /** @description Anime Stats */ + anime_stats?: { + /** + * Format: float + * @description Number of days spent watching Anime + */ + days_watched?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Anime Watching */ + watching?: number; + /** @description Anime Completed */ + completed?: number; + /** @description Anime On-Hold */ + on_hold?: number; + /** @description Anime Dropped */ + dropped?: number; + /** @description Anime Planned to Watch */ + plan_to_watch?: number; + /** @description Total Anime entries on User list */ + total_entries?: number; + /** @description Anime re-watched */ + rewatched?: number; + /** @description Number of Anime Episodes Watched */ + episodes_watched?: number; + }; + /** @description Manga Stats */ + manga_stats?: { + /** + * Format: float + * @description Number of days spent reading Manga + */ + days_read?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Manga Reading */ + reading?: number; + /** @description Manga Completed */ + completed?: number; + /** @description Manga On-Hold */ + on_hold?: number; + /** @description Manga Dropped */ + dropped?: number; + /** @description Manga Planned to Read */ + plan_to_read?: number; + /** @description Total Manga entries on User list */ + total_entries?: number; + /** @description Manga re-read */ + reread?: number; + /** @description Number of Manga Chapters Read */ + chapters_read?: number; + /** @description Number of Manga Volumes Read */ + volumes_read?: number; + }; + /** @description Favorite entries */ + favorites?: { + /** @description Favorite Anime */ + anime?: components['schemas']['entry_meta'][]; + /** @description Favorite Manga */ + manga?: components['schemas']['entry_meta'][]; + /** @description Favorite Characters */ + characters?: components['schemas']['entry_meta'][]; + /** @description Favorite People */ + people?: components['schemas']['entry_meta'][]; + }; + /** @description User About. NOTE: About information is customizable by users through BBCode on MyAnimeList. This means users can add multimedia content, different text sizes, etc. Due to this freeform, Jikan returns parsed HTML. Validate on your end! */ + about?: string; + }[]; + }; + user_statistics: { + data?: { + /** @description Anime Statistics */ + anime?: { + /** + * Format: float + * @description Number of days spent watching Anime + */ + days_watched?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Anime Watching */ + watching?: number; + /** @description Anime Completed */ + completed?: number; + /** @description Anime On-Hold */ + on_hold?: number; + /** @description Anime Dropped */ + dropped?: number; + /** @description Anime Planned to Watch */ + plan_to_watch?: number; + /** @description Total Anime entries on User list */ + total_entries?: number; + /** @description Anime re-watched */ + rewatched?: number; + /** @description Number of Anime Episodes Watched */ + episodes_watched?: number; + }; + /** @description Manga Statistics */ + manga?: { + /** + * Format: float + * @description Number of days spent reading Manga + */ + days_read?: number; + /** + * Format: float + * @description Mean Score + */ + mean_score?: number; + /** @description Manga Reading */ + reading?: number; + /** @description Manga Completed */ + completed?: number; + /** @description Manga On-Hold */ + on_hold?: number; + /** @description Manga Dropped */ + dropped?: number; + /** @description Manga Planned to Read */ + plan_to_read?: number; + /** @description Total Manga entries on User list */ + total_entries?: number; + /** @description Manga re-read */ + reread?: number; + /** @description Number of Manga Chapters Read */ + chapters_read?: number; + /** @description Number of Manga Volumes Read */ + volumes_read?: number; + }; + }; + }; + /** @description Recommendations */ + recommendations: { + data?: { + /** @description MAL IDs of recommendations is both of the MAL ID's with a `-` delimiter */ + mal_id?: string; + /** @description Array of 2 entries that are being recommended to each other */ + entry?: (components['schemas']['anime_meta'] | components['schemas']['manga_meta'])[]; + /** @description Recommendation context provided by the user */ + content?: string; + user?: components['schemas']['user_by_id']; + }[]; + } & components['schemas']['pagination']; + /** @description Entry Recommendations Resource */ + entry_recommendations: { + data?: { + entry?: components['schemas']['anime_meta'] | components['schemas']['manga_meta']; + }[]; + }; + manga_review: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList review URL */ + url?: string; + /** @description Entry type */ + type?: string; + /** @description User reaction count on the review */ + reactions?: { + /** @description Overall reaction count */ + overall?: number; + /** @description Nice reaction count */ + nice?: number; + /** @description Love it reaction count */ + love_it?: number; + /** @description Funny reaction count */ + funny?: number; + /** @description Confusing reaction count */ + confusing?: number; + /** @description Informative reaction count */ + informative?: number; + /** @description Well written reaction count */ + well_written?: number; + /** @description Creative reaction count */ + creative?: number; + }; + /** @description Review created date ISO8601 */ + date?: string; + /** @description Review content */ + review?: string; + /** @description Number of user votes on the Review */ + score?: number; + /** @description Review tags */ + tags?: string[]; + /** @description The review contains spoiler */ + is_spoiler?: boolean; + /** @description The review was made before the entry was completed */ + is_preliminary?: boolean; + }; + anime_review: { + /** @description MyAnimeList ID */ + mal_id?: number; + /** @description MyAnimeList review URL */ + url?: string; + /** @description Entry type */ + type?: string; + /** @description User reaction count on the review */ + reactions?: { + /** @description Overall reaction count */ + overall?: number; + /** @description Nice reaction count */ + nice?: number; + /** @description Love it reaction count */ + love_it?: number; + /** @description Funny reaction count */ + funny?: number; + /** @description Confusing reaction count */ + confusing?: number; + /** @description Informative reaction count */ + informative?: number; + /** @description Well written reaction count */ + well_written?: number; + /** @description Creative reaction count */ + creative?: number; + }; + /** @description Review created date ISO8601 */ + date?: string; + /** @description Review content */ + review?: string; + /** @description Number of user votes on the Review */ + score?: number; + /** @description Review tags */ + tags?: string[]; + /** @description The review contains spoiler */ + is_spoiler?: boolean; + /** @description The review was made before the entry was completed */ + is_preliminary?: boolean; + /** @description Number of episodes watched */ + episodes_watched?: number; + }; + /** @description Anime Reviews Resource */ + anime_reviews: { + data?: ({ + user?: components['schemas']['user_meta']; + } & components['schemas']['anime_review'])[]; + } & components['schemas']['pagination']; + /** @description Manga Reviews Resource */ + manga_reviews: { + data?: ({ + user?: components['schemas']['user_meta']; + } & components['schemas']['manga_review'])[]; + } & components['schemas']['pagination']; + /** @description Streaming links */ + streaming_links: { + data?: { + name?: string; + url?: string; + }[]; + }; + /** @description Anime User Updates Resource */ + anime_userupdates: { + data?: { + user?: components['schemas']['user_meta']; + /** @description User Score */ + score?: number | null; + /** @description User list status */ + status?: string; + /** @description Number of episodes seen */ + episodes_seen?: number | null; + /** @description Total number of episodes */ + episodes_total?: number | null; + /** @description Last updated date ISO8601 */ + date?: string; + }[]; + } & components['schemas']['pagination']; + /** @description Manga User Updates Resource */ + manga_userupdates: { + data?: { + user?: components['schemas']['user_meta']; + /** @description User Score */ + score?: number | null; + /** @description User list status */ + status?: string; + /** @description Number of volumes read */ + volumes_read?: number; + /** @description Total number of volumes */ + volumes_total?: number; + /** @description Number of chapters read */ + chapters_read?: number; + /** @description Total number of chapters */ + chapters_total?: number; + /** @description Last updated date ISO8601 */ + date?: string; + }[]; + } & components['schemas']['pagination']; + }; + responses: { + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + parameters: { + /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ + continuing: boolean; + /** @description This is a flag. When supplied it will include entries with the Kids genres in specific endpoints that filter them out by default. You do not need to pass a value to it. e.g usage: `?kids` */ + kids: boolean; + limit: number; + page: number; + /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ + preliminary: boolean; + /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ + sfw: boolean; + /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ + spoilers: boolean; + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved: boolean; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + getAnimeFullById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns complete anime resource data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['anime_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['anime']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeCharacters: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime characters resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_characters']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeStaff: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime staff resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_staff']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeEpisodes: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of anime episodes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_episodes']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeEpisodeById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + episode: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a single anime episode resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['anime_episode']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeNews: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of news articles related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_news']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeForum: { + parameters: { + query?: { + /** @description Filter topics */ + filter?: 'all' | 'episode' | 'other'; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of forum topics related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['forum']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeVideos: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns videos related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_videos']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeVideosEpisodes: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns episode videos related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_videos_episodes']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimePictures: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns pictures related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['pictures_variants']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeStatistics: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_statistics']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeMoreInfo: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['moreinfo']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeRecommendations: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime recommendations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['entry_recommendations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeUserUpdates: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of users who have added/updated/removed the entry on their list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_userupdates']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ + preliminary?: components['parameters']['preliminary']; + /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ + spoilers?: components['parameters']['spoilers']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_reviews']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeRelations: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime relations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['relation'][]; + }; + }; + }; + }; + }; + getAnimeThemes: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime themes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_themes']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeExternal: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime external links */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['external_links']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeStreaming: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime streaming links */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['external_links']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterFullById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns complete character resource data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['character_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns character resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['character']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterAnime: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime that character is in */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['character_anime']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterManga: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga that character is in */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['character_manga']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterVoiceActors: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns the character's voice actors */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['character_voice_actors']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharacterPictures: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns pictures related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['character_pictures']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getClubsById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Club Resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['club']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getClubMembers: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Club Members Resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['pagination'] & components['schemas']['club_member']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getClubStaff: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Club Staff */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['club_staff']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getClubRelations: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Club Relations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['club_relations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeGenres: { + parameters: { + query?: { + filter?: components['schemas']['genre_query_filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns entry genres, explicit_genres, themes and demographics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['genres']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaGenres: { + parameters: { + query?: { + filter?: components['schemas']['genre_query_filter']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns entry genres, explicit_genres, themes and demographics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['genres']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMagazines: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + order_by?: components['schemas']['magazines_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns magazines collection */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['magazines']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaFullById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns complete manga resource data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['manga_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns pictures related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['manga']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaCharacters: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga characters resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_characters']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaNews: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of manga news topics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_news']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaTopics: { + parameters: { + query?: { + /** @description Filter topics */ + filter?: 'all' | 'episode' | 'other'; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of manga forum topics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['forum']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaPictures: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of manga pictures */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_pictures']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaStatistics: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns anime statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_statistics']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaMoreInfo: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga moreinfo */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['moreinfo']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaRecommendations: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga recommendations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['entry_recommendations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaUserUpdates: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga user updates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_userupdates']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ + preliminary?: components['parameters']['preliminary']; + /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ + spoilers?: components['parameters']['spoilers']; + }; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_reviews']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaRelations: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga relations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['relation'][]; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaExternal: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns manga external links */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['external_links']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonFullById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns complete character resource data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['person_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns pictures related to the entry */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['person']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonAnime: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns person's anime staff positions */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['person_anime']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonVoices: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns person's voice acting roles */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['person_voice_acting_roles']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonManga: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns person's published manga works */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['person_manga']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPersonPictures: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a list of pictures of the person */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['person_pictures']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getProducerById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns producer resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['producer']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getProducerFullById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns producer resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['producer_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getProducerExternal: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns producer's external links */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['external_links']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRandomAnime: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a random anime resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['anime']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRandomManga: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a random manga resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['manga']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRandomCharacters: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a random character resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['character']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRandomPeople: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a random person resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['person']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRandomUsers: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a random user profile resource */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['user_profile']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecentAnimeRecommendations: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns recent anime recommendations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['recommendations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecentMangaRecommendations: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns recent manga recommendations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['recommendations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecentAnimeReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ + preliminary?: components['parameters']['preliminary']; + /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ + spoilers?: components['parameters']['spoilers']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns recent anime reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecentMangaReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ + preliminary?: components['parameters']['preliminary']; + /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ + spoilers?: components['parameters']['spoilers']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns recent manga reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSchedules: { + parameters: { + query?: { + /** @description Filter by day */ + filter?: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' | 'unknown' | 'other'; + /** @description When supplied, it will filter entries with the `Kids` Genre Demographic. When supplied as `kids=true`, it will return only Kid entries and when supplied as `kids=false`, it will filter out any Kid entries. Defaults to `false`. */ + kids?: 'true' | 'false'; + /** @description 'Safe For Work'. When supplied, it will filter entries with the `Hentai` Genre. When supplied as `sfw=true`, it will return only SFW entries and when supplied as `sfw=false`, it will filter out any Hentai entries. Defaults to `false`. */ + sfw?: 'true' | 'false'; + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns weekly schedule */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['schedules']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAnimeSearch: { + parameters: { + query?: { + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + type?: components['schemas']['anime_search_query_type']; + score?: number; + /** @description Set a minimum score for results. */ + min_score?: number; + /** @description Set a maximum score for results */ + max_score?: number; + status?: components['schemas']['anime_search_query_status']; + rating?: components['schemas']['anime_search_query_rating']; + /** @description Filter out Adult entries */ + sfw?: boolean; + /** @description Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + genres?: string; + /** @description Exclude genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + genres_exclude?: string; + order_by?: components['schemas']['anime_search_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + /** @description Filter by producer(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + producers?: string; + /** @description Filter by starting date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ + start_date?: string; + /** @description Filter by ending date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ + end_date?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for anime */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMangaSearch: { + parameters: { + query?: { + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + type?: components['schemas']['manga_search_query_type']; + score?: number; + /** @description Set a minimum score for results. */ + min_score?: number; + /** @description Set a maximum score for results */ + max_score?: number; + status?: components['schemas']['manga_search_query_status']; + /** @description Filter out Adult entries */ + sfw?: boolean; + /** @description Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + genres?: string; + /** @description Exclude genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + genres_exclude?: string; + order_by?: components['schemas']['manga_search_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + /** @description Filter by magazine(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ + magazines?: string; + /** @description Filter by starting date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ + start_date?: string; + /** @description Filter by ending date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ + end_date?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for manga */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getPeopleSearch: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + order_by?: components['schemas']['people_search_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for people */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['people_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getCharactersSearch: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + order_by?: components['schemas']['characters_search_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for characters */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['characters_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUsersSearch: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + gender?: components['schemas']['users_search_query_gender']; + location?: string; + maxAge?: number; + minAge?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['users_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserById: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns username by ID search */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['user_by_id']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getClubsSearch: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + type?: components['schemas']['club_search_query_type']; + category?: components['schemas']['club_search_query_category']; + order_by?: components['schemas']['club_search_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns search results for clubs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['clubs_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getProducers: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + q?: string; + order_by?: components['schemas']['producers_query_orderby']; + sort?: components['schemas']['search_query_sort']; + /** @description Return entries starting with the given letter */ + letter?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns producers collection */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['producers']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSeasonNow: { + parameters: { + query?: { + /** @description Entry types */ + filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; + /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ + sfw?: components['parameters']['sfw']; + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ + continuing?: components['parameters']['continuing']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns current seasonal anime */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSeason: { + parameters: { + query?: { + /** @description Entry types */ + filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; + /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ + sfw?: components['parameters']['sfw']; + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ + continuing?: components['parameters']['continuing']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path: { + year: number; + season: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns seasonal anime */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSeasonsList: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns available list of seasons */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['seasons']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSeasonUpcoming: { + parameters: { + query?: { + /** @description Entry types */ + filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; + /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ + sfw?: components['parameters']['sfw']; + /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ + unapproved?: components['parameters']['unapproved']; + /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ + continuing?: components['parameters']['continuing']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns upcoming season's anime */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getTopAnime: { + parameters: { + query?: { + type?: components['schemas']['anime_search_query_type']; + filter?: components['schemas']['top_anime_filter']; + rating?: components['schemas']['anime_search_query_rating']; + /** @description Filter out Adult entries */ + sfw?: boolean; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns top anime */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['anime_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getTopManga: { + parameters: { + query?: { + type?: components['schemas']['manga_search_query_type']; + filter?: components['schemas']['top_manga_filter']; + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns top manga */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['manga_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getTopPeople: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns top people */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['people_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getTopCharacters: { + parameters: { + query?: { + page?: components['parameters']['page']; + limit?: components['parameters']['limit']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns top characters */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['characters_search']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getTopReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + type?: components['schemas']['top_reviews_type_enum']; + /** @description Whether the results include preliminary reviews or not. Defaults to true. */ + preliminary?: boolean; + /** @description Whether the results include reviews with spoilers or not. Defaults to true. */ + spoilers?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns top reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: { + data?: ( + | ({ + user?: components['schemas']['user_meta']; + } & { + anime?: components['schemas']['anime_meta']; + } & components['schemas']['anime_review']) + | ({ + user?: components['schemas']['user_meta']; + } & { + manga?: components['schemas']['manga_meta']; + } & components['schemas']['manga_review']) + )[]; + } & components['schemas']['pagination']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserFullProfile: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns complete user resource data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['user_profile_full']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserProfile: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user profile */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['user_profile']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserStatistics: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_statistics']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserFavorites: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user favorites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: components['schemas']['user_favorites']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserUpdates: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user updates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_updates']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserAbout: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user about in raw HTML */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_about']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserHistory: { + parameters: { + query?: { + type?: 'anime' | 'manga'; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user history (past 30 days) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_history']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserFriends: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user friends */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_friends']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserAnimelist: { + parameters: { + query?: { + status?: components['schemas']['user_anime_list_status_filter']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user anime list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserMangaList: { + parameters: { + query?: { + status?: components['schemas']['user_manga_list_status_filter']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user manga list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserReviews: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user reviews */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data?: { + data?: ( + | ({ + user?: components['schemas']['user_meta']; + } & { + anime?: components['schemas']['anime_meta']; + } & components['schemas']['anime_review']) + | ({ + user?: components['schemas']['user_meta']; + } & { + manga?: components['schemas']['manga_meta']; + } & components['schemas']['manga_review']) + )[]; + } & components['schemas']['pagination']; + }; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserRecommendations: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Recent Anime Recommendations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['recommendations']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserClubs: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user clubs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['user_clubs']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getUserExternal: { + parameters: { + query?: never; + header?: never; + path: { + username: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user's external links */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['external_links']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getWatchRecentEpisodes: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Recently Added Episodes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['watch_episodes']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getWatchPopularEpisodes: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Popular Episodes */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['watch_episodes']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getWatchRecentPromos: { + parameters: { + query?: { + page?: components['parameters']['page']; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Recently Added Promotional Videos */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['watch_promos']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getWatchPopularPromos: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns Popular Promotional Videos */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['watch_promos']; + }; + }; + /** @description Error: Bad request. When required parameters were not supplied. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; +} diff --git a/api/schemas/OpenLibrary.json b/api/schemas/OpenLibrary.json new file mode 100644 index 00000000..6258f57e --- /dev/null +++ b/api/schemas/OpenLibrary.json @@ -0,0 +1,602 @@ +{ + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "type": "string" + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object" + } + } + }, + "info": { + "description": "- These are still in development and may not be perfect\n- Contribute by proposing edits to [openapi.json](https://github.com/internetarchive/openlibrary/blob/master/static/openapi.json)\n- Please do not use our APIs for bulk downloads, see [dev center](https://openlibrary.org/developers/api)", + "title": "Open Library API", + "version": "0.1.0" + }, + "openapi": "3.0.2", + "paths": { + "/api/books": { + "get": { + "operationId": "read_api_books_api_books_get", + "parameters": [ + { + "examples": { + "isbn": { + "value": "ISBN:0201558025" + }, + "multiple": { + "value": "ISBN:9781408113479,OCLC:420517" + }, + "oclc": { + "value": "OCLC:263296519" + } + }, + "in": "query", + "name": "bibkeys", + "required": true, + "schema": { + "title": "Bibkeys", + "type": "string" + } + }, + { + "description": "Specifies the response format. Possible values are json and javascript. When not specified the format is javascript.", + "in": "query", + "name": "format", + "required": false, + "schema": { + "default": "json", + "title": "Format", + "type": "string" + } + }, + { + "description": "The name of the JavaScript function to call with the result. This is considered only when the format is javascript.", + "in": "query", + "name": "callback", + "required": false, + "schema": { + "title": "Callback" + } + }, + { + "description": "Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi.", + "in": "query", + "name": "jscmd", + "required": false, + "schema": { + "default": "viewapi", + "title": "Jscmd", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Api Books", + "tags": ["books"] + } + }, + "/api/volumes/brief/{key_type}/{value}.json": { + "get": { + "operationId": "read_api_volumes_brief_api_volumes_brief__key_type___value__json_get", + "parameters": [ + { + "in": "path", + "name": "key_type", + "required": true, + "schema": { + "title": "Key Type" + } + }, + { + "in": "path", + "name": "value", + "required": true, + "schema": { + "title": "Value" + } + }, + { + "in": "query", + "name": "callback", + "required": false, + "schema": { + "title": "Callback" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Api Volumes Brief", + "tags": ["books"] + } + }, + "/authors/{olid}.json": { + "get": { + "operationId": "read_authors_authors__olid__json_get", + "parameters": [ + { + "in": "path", + "name": "olid", + "required": true, + "schema": { + "title": "Olid" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Authors", + "tags": ["authors"] + } + }, + "/authors/{olid}/works.json": { + "get": { + "operationId": "read_authors_works_authors__olid__works_json_get", + "parameters": [ + { + "in": "path", + "name": "olid", + "required": true, + "schema": { + "title": "Olid" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "title": "Limit", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Authors Works", + "tags": ["authors"] + } + }, + "/books/{olid}": { + "get": { + "operationId": "read_books_books__olid__get", + "parameters": [ + { + "in": "path", + "name": "olid", + "required": true, + "schema": { + "example": "OL53924W", + "title": "Olid" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Books", + "tags": ["books"] + } + }, + "/covers/{key_type}/{value}-{size}.jpg": { + "get": { + "operationId": "read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get", + "parameters": [ + { + "in": "path", + "name": "key_type", + "required": true, + "schema": { + "title": "Key Type" + } + }, + { + "in": "path", + "name": "value", + "required": true, + "schema": { + "title": "Value" + } + }, + { + "in": "path", + "name": "size", + "required": true, + "schema": { + "title": "Size" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Covers Key Type Value Size Jpeg", + "tags": ["covers"] + } + }, + "/isbn/{isbn}": { + "get": { + "operationId": "read_isbn_isbn__isbn__get", + "parameters": [ + { + "in": "path", + "name": "isbn", + "required": true, + "schema": { + "title": "Isbn" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Isbn", + "tags": ["books"] + } + }, + "/search.json": { + "get": { + "operationId": "read_search_json_search_json_get", + "parameters": [ + { + "in": "query", + "name": "q", + "required": true, + "schema": { + "title": "Q" + } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "title": "Page", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Search Json", + "tags": ["search"] + } + }, + "/search/authors.json": { + "get": { + "operationId": "read_search_authors_json_search_authors_json_get", + "parameters": [ + { + "in": "query", + "name": "q", + "required": true, + "schema": { + "title": "Q" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Search Authors Json", + "tags": ["search"] + } + }, + "/subjects/{subject}.json": { + "get": { + "operationId": "read_subjects_subjects__subject__json_get", + "parameters": [ + { + "in": "path", + "name": "subject", + "required": true, + "schema": { + "title": "Subject" + } + }, + { + "in": "query", + "name": "details", + "required": false, + "schema": { + "default": false, + "title": "Details", + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Subjects", + "tags": ["subjects"] + } + }, + "/works/{olid}": { + "get": { + "operationId": "read_works_works__olid__get", + "parameters": [ + { + "in": "path", + "name": "olid", + "required": true, + "schema": { + "title": "Olid" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Read Works", + "tags": ["books"] + } + } + }, + "tags": [ + { + "description": "Retrieve a specific work or edition by identifier", + "externalDocs": { + "description": "Find out more", + "url": "https://openlibrary.org/dev/docs/api/books" + }, + "name": "books" + }, + { + "description": "Retrieve an author and their works by author identifier", + "externalDocs": { + "description": "Find out more", + "url": "https://openlibrary.org/dev/docs/api/authors" + }, + "name": "authors" + }, + { + "description": "Search results for books, authors, and more", + "externalDocs": { + "description": "Find out more", + "url": "https://openlibrary.org/dev/docs/api/search" + }, + "name": "search" + }, + { + "description": "Fetch book covers by ISBN or Open Library identifier", + "externalDocs": { + "description": "Find out more", + "url": "https://openlibrary.org/dev/docs/api/covers" + }, + "name": "covers" + }, + { + "description": "Fetch books by subject name ", + "externalDocs": { + "description": "Find out more", + "url": "https://openlibrary.org/dev/docs/api/subjects" + }, + "name": "subjects" + } + ] +} diff --git a/api/schemas/OpenLibrary.ts b/api/schemas/OpenLibrary.ts new file mode 100644 index 00000000..aa6fbbac --- /dev/null +++ b/api/schemas/OpenLibrary.ts @@ -0,0 +1,578 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/api/books': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Api Books */ + get: operations['read_api_books_api_books_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/volumes/brief/{key_type}/{value}.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Api Volumes Brief */ + get: operations['read_api_volumes_brief_api_volumes_brief__key_type___value__json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/authors/{olid}.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Authors */ + get: operations['read_authors_authors__olid__json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/authors/{olid}/works.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Authors Works */ + get: operations['read_authors_works_authors__olid__works_json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/books/{olid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Books */ + get: operations['read_books_books__olid__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/covers/{key_type}/{value}-{size}.jpg': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Covers Key Type Value Size Jpeg */ + get: operations['read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/isbn/{isbn}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Isbn */ + get: operations['read_isbn_isbn__isbn__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/search.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Search Json */ + get: operations['read_search_json_search_json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/search/authors.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Search Authors Json */ + get: operations['read_search_authors_json_search_authors_json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/subjects/{subject}.json': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Subjects */ + get: operations['read_subjects_subjects__subject__json_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/works/{olid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Read Works */ + get: operations['read_works_works__olid__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components['schemas']['ValidationError'][]; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: string[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + read_api_books_api_books_get: { + parameters: { + query: { + bibkeys: string; + /** @description Specifies the response format. Possible values are json and javascript. When not specified the format is javascript. */ + format?: string; + /** @description The name of the JavaScript function to call with the result. This is considered only when the format is javascript. */ + callback?: unknown; + /** @description Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi. */ + jscmd?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_api_volumes_brief_api_volumes_brief__key_type___value__json_get: { + parameters: { + query?: { + callback?: unknown; + }; + header?: never; + path: { + key_type: unknown; + value: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_authors_authors__olid__json_get: { + parameters: { + query?: never; + header?: never; + path: { + olid: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_authors_works_authors__olid__works_json_get: { + parameters: { + query?: { + limit?: number; + }; + header?: never; + path: { + olid: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_books_books__olid__get: { + parameters: { + query?: never; + header?: never; + path: { + olid: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get: { + parameters: { + query?: never; + header?: never; + path: { + key_type: unknown; + value: unknown; + size: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_isbn_isbn__isbn__get: { + parameters: { + query?: never; + header?: never; + path: { + isbn: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_search_json_search_json_get: { + parameters: { + query: { + q: unknown; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_search_authors_json_search_authors_json_get: { + parameters: { + query: { + q: unknown; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_subjects_subjects__subject__json_get: { + parameters: { + query?: { + details?: boolean; + }; + header?: never; + path: { + subject: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + read_works_works__olid__get: { + parameters: { + query?: never; + header?: never; + path: { + olid: unknown; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; +} diff --git a/api/schemas/TMDB.ts b/api/schemas/TMDB.ts new file mode 100644 index 00000000..6df120e0 --- /dev/null +++ b/api/schemas/TMDB.ts @@ -0,0 +1,22832 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/3/authentication': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Validate Key + * @description Test your API Key to see if it's valid. + */ + get: operations['authentication-validate-key']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get the public details of an account on TMDB. + */ + get: operations['account-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/favorite': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Favorite + * @description Mark a movie or TV show as a favourite. + */ + post: operations['account-add-favorite']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/watchlist': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add To Watchlist + * @description Add a movie or TV show to your watchlist. + */ + post: operations['account-add-to-watchlist']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/favorite/movies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Favorite Movies + * @description Get a users list of favourite movies. + */ + get: operations['account-get-favorites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/favorite/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Favorite TV + * @description Get a users list of favourite TV shows. + */ + get: operations['account-favorite-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/lists': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Lists + * @description Get a users list of custom lists. + */ + get: operations['account-lists']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/rated/movies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated Movies + * @description Get a users list of rated movies. + */ + get: operations['account-rated-movies']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/rated/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated TV + * @description Get a users list of rated TV shows. + */ + get: operations['account-rated-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/rated/tv/episodes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated TV Episodes + * @description Get a users list of rated TV episodes. + */ + get: operations['account-rated-tv-episodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/watchlist/movies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Watchlist Movies + * @description Get a list of movies added to a users watchlist. + */ + get: operations['account-watchlist-movies']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/account/{account_id}/watchlist/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Watchlist TV + * @description Get a list of TV shows added to a users watchlist. + */ + get: operations['account-watchlist-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/guest_session/new': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Create Guest Session */ + get: operations['authentication-create-guest-session']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/token/new': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Create Request Token */ + get: operations['authentication-create-request-token']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/session/new': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create Session */ + post: operations['authentication-create-session']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/session/convert/4': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create Session (from v4 token) */ + post: operations['authentication-create-session-from-v4-token']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/token/validate_with_login': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Session (with login) + * @description This method allows an application to validate a request token by entering a username and password. + */ + post: operations['authentication-create-session-from-login']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/authentication/session': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Session */ + delete: operations['authentication-delete-session']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/certification/movie/list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie Certifications + * @description Get an up to date list of the officially supported movie certifications on TMDB. + */ + get: operations['certification-movie-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/certification/tv/list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** TV Certifications */ + get: operations['certifications-tv-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie List + * @description Get a list of all of the movie ids that have been changed in the past 24 hours. + */ + get: operations['changes-movie-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** People List */ + get: operations['changes-people-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** TV List */ + get: operations['changes-tv-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/collection/{collection_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get collection details by ID. + */ + get: operations['collection-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/collection/{collection_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the images that belong to a collection. + */ + get: operations['collection-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/collection/{collection_id}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Translations */ + get: operations['collection-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/company/{company_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get the company details by ID. + */ + get: operations['company-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/company/{company_id}/alternative_names': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Alternative Names + * @description Get the company details by ID. + */ + get: operations['company-alternative-names']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/company/{company_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the company logos by id. + */ + get: operations['company-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Query the API configuration details. + */ + get: operations['configuration-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration/countries': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Countries + * @description Get the list of countries (ISO 3166-1 tags) used throughout TMDB. + */ + get: operations['configuration-countries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration/jobs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Jobs + * @description Get the list of the jobs and departments we use on TMDB. + */ + get: operations['configuration-jobs']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration/languages': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Languages + * @description Get the list of languages (ISO 639-1 tags) used throughout TMDB. + */ + get: operations['configuration-languages']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration/primary_translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Primary Translations + * @description Get a list of the officially supported translations on TMDB. + */ + get: operations['configuration-primary-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/configuration/timezones': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Timezones + * @description Get the list of timezones used throughout TMDB. + */ + get: operations['configuration-timezones']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/credit/{credit_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get a movie or TV credit details by ID. + */ + get: operations['credit-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/discover/movie': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie + * @description Find movies using over 30 filters and sort options. + */ + get: operations['discover-movie']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/discover/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV + * @description Find TV shows using over 30 filters and sort options. + */ + get: operations['discover-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/find/{external_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Find By ID + * @description Find data by external ID's. + */ + get: operations['find-by-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/genre/movie/list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie List + * @description Get the list of official genres for movies. + */ + get: operations['genre-movie-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/genre/tv/list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV List + * @description Get the list of official genres for TV shows. + */ + get: operations['genre-tv-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/guest_session/{guest_session_id}/rated/movies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated Movies + * @description Get the rated movies for a guest session. + */ + get: operations['guest-session-rated-movies']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/guest_session/{guest_session_id}/rated/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated TV + * @description Get the rated TV shows for a guest session. + */ + get: operations['guest-session-rated-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/guest_session/{guest_session_id}/rated/tv/episodes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Rated TV Episodes + * @description Get the rated TV episodes for a guest session. + */ + get: operations['guest-session-rated-tv-episodes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/keyword/{keyword_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Details */ + get: operations['keyword-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/keyword/{keyword_id}/movies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Movies */ + get: operations['keyword-movies']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list/{list_id}/add_item': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Movie + * @description Add a movie to a list. + */ + post: operations['list-add-movie']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list/{list_id}/item_status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Check Item Status + * @description Use this method to check if an item has already been added to the list. + */ + get: operations['list-check-item-status']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list/{list_id}/clear': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear + * @description Clear all items from a list. + */ + post: operations['list-clear']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create */ + post: operations['list-create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list/{list_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Details */ + get: operations['list-details']; + put?: never; + post?: never; + /** + * Delete + * @description Delete a list. + */ + delete: operations['list-delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/list/{list_id}/remove_item': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove Movie + * @description Remove a movie from a list. + */ + post: operations['list-remove-movie']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/now_playing': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Now Playing + * @description Get a list of movies that are currently in theatres. + */ + get: operations['movie-now-playing-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/popular': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Popular + * @description Get a list of movies ordered by popularity. + */ + get: operations['movie-popular-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/top_rated': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Top Rated + * @description Get a list of movies ordered by rating. + */ + get: operations['movie-top-rated-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/upcoming': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Upcoming + * @description Get a list of movies that are being released soon. + */ + get: operations['movie-upcoming-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get the top level details of a movie by ID. + */ + get: operations['movie-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/account_states': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Account States + * @description Get the rating, watchlist and favourite status of an account. + */ + get: operations['movie-account-states']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/alternative_titles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Alternative Titles + * @description Get the alternative titles for a movie. + */ + get: operations['movie-alternative-titles']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Changes + * @description Get the recent changes for a movie. + */ + get: operations['movie-changes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Credits */ + get: operations['movie-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/external_ids': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** External IDs */ + get: operations['movie-external-ids']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the images that belong to a movie. + */ + get: operations['movie-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/keywords': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Keywords */ + get: operations['movie-keywords']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/latest': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Latest + * @description Get the newest movie ID. + */ + get: operations['movie-latest-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/lists': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Lists + * @description Get the lists that a movie has been added to. + */ + get: operations['movie-lists']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/recommendations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Recommendations */ + get: operations['movie-recommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/release_dates': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Release Dates + * @description Get the release dates and certifications for a movie. + */ + get: operations['movie-release-dates']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Reviews + * @description Get the user reviews for a movie. + */ + get: operations['movie-reviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/similar': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Similar + * @description Get the similar movies based on genres and keywords. + */ + get: operations['movie-similar']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Translations + * @description Get the translations for a movie. + */ + get: operations['movie-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Videos */ + get: operations['movie-videos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/watch/providers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Watch Providers + * @description Get the list of streaming providers we have for a movie. + */ + get: operations['movie-watch-providers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/movie/{movie_id}/rating': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Rating + * @description Rate a movie and save it to your rated list. + */ + post: operations['movie-add-rating']; + /** + * Delete Rating + * @description Delete a user rating. + */ + delete: operations['movie-delete-rating']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/network/{network_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Details */ + get: operations['network-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/network/{network_id}/alternative_names': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Alternative Names + * @description Get the alternative names of a network. + */ + get: operations['details-copy']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/network/{network_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the TV network logos by id. + */ + get: operations['alternative-names-copy']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/popular': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Popular + * @description Get a list of people ordered by popularity. + */ + get: operations['person-popular-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Query the top level details of a person. + */ + get: operations['person-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Changes + * @description Get the recent changes for a person. + */ + get: operations['person-changes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/combined_credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Combined Credits + * @description Get the combined movie and TV credits that belong to a person. + */ + get: operations['person-combined-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/external_ids': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * External IDs + * @description Get the external ID's that belong to a person. + */ + get: operations['person-external-ids']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the profile images that belong to a person. + */ + get: operations['person-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/latest': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Latest + * @description Get the newest created person. This is a live response and will continuously change. + */ + get: operations['person-latest-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/movie_credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie Credits + * @description Get the movie credits for a person. + */ + get: operations['person-movie-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/tv_credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV Credits + * @description Get the TV credits that belong to a person. + */ + get: operations['person-tv-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/tagged_images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Tagged Images + * @description Get the tagged images for a person. + */ + get: operations['person-tagged-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/person/{person_id}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Translations + * @description Get the translations that belong to a person. + */ + get: operations['translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/review/{review_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Retrieve the details of a movie or TV show review. + */ + get: operations['review-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/collection': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Collection + * @description Search for collections by their original, translated and alternative names. + */ + get: operations['search-collection']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/company': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Company + * @description Search for companies by their original and alternative names. + */ + get: operations['search-company']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/keyword': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Keyword + * @description Search for keywords by their name. + */ + get: operations['search-keyword']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/movie': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie + * @description Search for movies by their original, translated and alternative titles. + */ + get: operations['search-movie']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/multi': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Multi + * @description Use multi search when you want to search for movies, TV shows and people in a single request. + */ + get: operations['search-multi']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/person': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Person + * @description Search for people by their name and also known as names. + */ + get: operations['search-person']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/search/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV + * @description Search for TV shows by their original, translated and also known as names. + */ + get: operations['search-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/trending/all/{time_window}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * All + * @description Get the trending movies, TV shows and people. + */ + get: operations['trending-all']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/trending/movie/{time_window}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movies + * @description Get the trending movies on TMDB. + */ + get: operations['trending-movies']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/trending/person/{time_window}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * People + * @description Get the trending people on TMDB. + */ + get: operations['trending-people']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/trending/tv/{time_window}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV + * @description Get the trending TV shows on TMDB. + */ + get: operations['trending-tv']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/airing_today': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Airing Today + * @description Get a list of TV shows airing today. + */ + get: operations['tv-series-airing-today-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/on_the_air': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * On The Air + * @description Get a list of TV shows that air in the next 7 days. + */ + get: operations['tv-series-on-the-air-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/popular': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Popular + * @description Get a list of TV shows ordered by popularity. + */ + get: operations['tv-series-popular-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/top_rated': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Top Rated + * @description Get a list of TV shows ordered by rating. + */ + get: operations['tv-series-top-rated-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get the details of a TV show. + */ + get: operations['tv-series-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/account_states': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Account States + * @description Get the rating, watchlist and favourite status. + */ + get: operations['tv-series-account-states']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/aggregate_credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Aggregate Credits + * @description Get the aggregate credits (cast and crew) that have been added to a TV show. + */ + get: operations['tv-series-aggregate-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/alternative_titles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Alternative Titles + * @description Get the alternative titles that have been added to a TV show. + */ + get: operations['tv-series-alternative-titles']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Changes + * @description Get the recent changes for a TV show. + */ + get: operations['tv-series-changes']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/content_ratings': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Content Ratings + * @description Get the content ratings that have been added to a TV show. + */ + get: operations['tv-series-content-ratings']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Credits + * @description Get the latest season credits of a TV show. + */ + get: operations['tv-series-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/episode_groups': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Episode Groups + * @description Get the episode groups that have been added to a TV show. + */ + get: operations['tv-series-episode-groups']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/external_ids': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * External IDs + * @description Get a list of external IDs that have been added to a TV show. + */ + get: operations['tv-series-external-ids']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the images that belong to a TV series. + */ + get: operations['tv-series-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/keywords': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Keywords + * @description Get a list of keywords that have been added to a TV show. + */ + get: operations['tv-series-keywords']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/latest': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Latest + * @description Get the newest TV show ID. + */ + get: operations['tv-series-latest-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/lists': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Lists + * @description Get the lists that a TV series has been added to. + */ + get: operations['lists-copy']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/recommendations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Recommendations */ + get: operations['tv-series-recommendations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/reviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Reviews + * @description Get the reviews that have been added to a TV show. + */ + get: operations['tv-series-reviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/screened_theatrically': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Screened Theatrically + * @description Get the seasons and episodes that have screened theatrically. + */ + get: operations['tv-series-screened-theatrically']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/similar': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Similar + * @description Get the similar TV shows. + */ + get: operations['tv-series-similar']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Translations + * @description Get the translations that have been added to a TV show. + */ + get: operations['tv-series-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Videos + * @description Get the videos that belong to a TV show. + */ + get: operations['tv-series-videos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/watch/providers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Watch Providers + * @description Get the list of streaming providers we have for a TV show. + */ + get: operations['tv-series-watch-providers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/rating': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Rating + * @description Rate a TV show and save it to your rated list. + */ + post: operations['tv-series-add-rating']; + /** Delete Rating */ + delete: operations['tv-series-delete-rating']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Query the details of a TV season. + */ + get: operations['tv-season-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/account_states': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Account States + * @description Get the rating, watchlist and favourite status. + */ + get: operations['tv-season-account-states']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/aggregate_credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Aggregate Credits + * @description Get the aggregate credits (cast and crew) that have been added to a TV season. + */ + get: operations['tv-season-aggregate-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/season/{season_id}/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Changes + * @description Get the recent changes for a TV season. + */ + get: operations['tv-season-changes-by-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Credits */ + get: operations['tv-season-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/external_ids': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * External IDs + * @description Get a list of external IDs that have been added to a TV season. + */ + get: operations['tv-season-external-ids']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the images that belong to a TV season. + */ + get: operations['tv-season-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Translations + * @description Get the translations for a TV season. + */ + get: operations['tv-season-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Videos + * @description Get the videos that belong to a TV season. + */ + get: operations['tv-season-videos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/watch/providers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Watch Providers + * @description Get the list of streaming providers we have for a TV season. + */ + get: operations['tv-season-watch-providers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Query the details of a TV episode. + */ + get: operations['tv-episode-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/account_states': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Account States + * @description Get the rating, watchlist and favourite status. + */ + get: operations['tv-episode-account-states']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/episode/{episode_id}/changes': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Changes + * @description Get the recent changes for a TV episode. + */ + get: operations['tv-episode-changes-by-id']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/credits': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Credits */ + get: operations['tv-episode-credits']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/external_ids': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * External IDs + * @description Get a list of external IDs that have been added to a TV episode. + */ + get: operations['tv-episode-external-ids']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Images + * @description Get the images that belong to a TV episode. + */ + get: operations['tv-episode-images']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Translations + * @description Get the translations that have been added to a TV episode. + */ + get: operations['tv-episode-translations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/videos': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Videos + * @description Get the videos that belong to a TV episode. + */ + get: operations['tv-episode-videos']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/rating': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Rating + * @description Rate a TV episode and save it to your rated list. + */ + post: operations['tv-episode-add-rating']; + /** + * Delete Rating + * @description Delete your rating on a TV episode. + */ + delete: operations['tv-episode-delete-rating']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/tv/episode_group/{tv_episode_group_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Details + * @description Get the details of a TV episode group. + */ + get: operations['tv-episode-group-details']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/watch/providers/regions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Available Regions + * @description Get the list of the countries we have watch provider (OTT/streaming) data for. + */ + get: operations['watch-providers-available-regions']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/watch/providers/movie': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Movie Providers + * @description Get the list of streaming providers we have for movies. + */ + get: operations['watch-providers-movie-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/3/watch/providers/tv': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * TV Providers + * @description Get the list of streaming providers we have for TV shows. + */ + get: operations['watch-provider-tv-list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + 'authentication-validate-key': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + /** @description 401 */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 7 + */ + status_code: number; + /** @example Invalid API key: You must be granted a valid key. */ + status_message?: string; + /** + * @default true + * @example false + */ + success: boolean; + }; + }; + }; + }; + }; + 'account-details': { + parameters: { + query?: { + session_id?: string; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + avatar?: { + gravatar?: { + /** @example c9e9fc152ee756a900db85757c29815d */ + hash?: string; + }; + tmdb?: { + /** @example /xy44UvpbTgzs9kWmp4C3fEaCl5h.png */ + avatar_path?: string; + }; + }; + /** + * @default 0 + * @example 548 + */ + id: number; + /** @example en */ + iso_639_1?: string; + /** @example CA */ + iso_3166_1?: string; + /** @example Travis Bell */ + name?: string; + /** + * @default true + * @example false + */ + include_adult: boolean; + /** @example travisbell */ + username?: string; + }; + }; + }; + }; + }; + 'account-add-favorite': { + parameters: { + query?: { + session_id?: string; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + }; + }; + 'account-add-to-watchlist': { + parameters: { + query?: { + session_id?: string; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + }; + }; + 'account-get-favorites': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /se5Hxz7PArQZOG3Nx2bpfOhLhtV.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 9806 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example The Incredibles */ + original_title?: string; + /** @example Bob Parr has given up his superhero days to log in time as an insurance adjuster and raise his three children with his formerly heroic wife in suburbia. But when he receives a mysterious assignment, it's time to get back into costume. */ + overview?: string; + /** + * @default 0 + * @example 71.477 + */ + popularity: number; + /** @example /2LqaLgk4Z226KkgPJuiOQ58wvrm.jpg */ + poster_path?: string; + /** @example 2004-10-27 */ + release_date?: string; + /** @example The Incredibles */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.702 + */ + vote_average: number; + /** + * @default 0 + * @example 16162 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages: number; + /** + * @default 0 + * @example 80 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-favorite-tv': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1396 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Breaking Bad */ + original_name?: string; + /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ + overview?: string; + /** + * @default 0 + * @example 292.904 + */ + popularity: number; + /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ + poster_path?: string; + /** @example 2008-01-20 */ + first_air_date?: string; + /** @example Breaking Bad */ + name?: string; + /** + * @default 0 + * @example 8.878 + */ + vote_average: number; + /** + * @default 0 + * @example 11548 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages: number; + /** + * @default 0 + * @example 68 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-lists': { + parameters: { + query?: { + page?: number; + session_id?: string; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example */ + description?: string; + /** + * @default 0 + * @example 0 + */ + favorite_count: number; + /** + * @default 0 + * @example 120174 + */ + id: number; + /** + * @default 0 + * @example 5 + */ + item_count: number; + /** @example en */ + iso_639_1?: string; + /** @example movie */ + list_type?: string; + /** @example Test Alpha Sort */ + name?: string; + poster_path?: unknown; + }[]; + /** + * @default 0 + * @example 2 + */ + total_pages: number; + /** + * @default 0 + * @example 25 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-rated-movies': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /dUVbWINfRMGojGZRcO6GF1Z2nV8.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 120 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example The Lord of the Rings: The Fellowship of the Ring */ + original_title?: string; + /** @example Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed. */ + overview?: string; + /** + * @default 0 + * @example 84.737 + */ + popularity: number; + /** @example /6oom5QYQ2yQTMJIbnvbkBL9cHo6.jpg */ + poster_path?: string; + /** @example 2001-12-18 */ + release_date?: string; + /** @example The Lord of the Rings: The Fellowship of the Ring */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.396 + */ + vote_average: number; + /** + * @default 0 + * @example 22579 + */ + vote_count: number; + /** + * @default 0 + * @example 8 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 47 + */ + total_pages: number; + /** + * @default 0 + * @example 940 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-rated-tv': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /2yZXtM2Kky1Sy0kachbDlwybl3y.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1705 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Fringe */ + original_name?: string; + /** @example FBI Special Agent Olivia Dunham, brilliant but formerly institutionalized scientist Walter Bishop and his scheming, reluctant son Peter uncover a deadly mystery involving a series of unbelievable events and realize they may be a part of a larger, more disturbing pattern that blurs the line between science fiction and technology. */ + overview?: string; + /** + * @default 0 + * @example 151.906 + */ + popularity: number; + /** @example /sY9hg5dLJ93RJOyKEiu1nAtBRND.jpg */ + poster_path?: string; + /** @example 2008-09-09 */ + first_air_date?: string; + /** @example Fringe */ + name?: string; + /** + * @default 0 + * @example 8.109 + */ + vote_average: number; + /** + * @default 0 + * @example 2050 + */ + vote_count: number; + /** + * @default 0 + * @example 9 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 15 + */ + total_pages: number; + /** + * @default 0 + * @example 290 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-rated-tv-episodes': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example 2013-10-17 */ + air_date?: string; + /** + * @default 0 + * @example 5 + */ + episode_number: number; + /** + * @default 0 + * @example 64782 + */ + id: number; + /** @example The Workplace Proximity */ + name?: string; + /** @example Amy starts working at Caltech which causes friction with Sheldon. Howard agrees with Sheldon who mentions this to Bernadette causing a big fight for the Wolowitzes. */ + overview?: string; + /** @example 4X5305 */ + production_code?: string; + /** + * @default 0 + * @example 22 + */ + runtime: number; + /** + * @default 0 + * @example 7 + */ + season_number: number; + /** + * @default 0 + * @example 1418 + */ + show_id: number; + /** @example /k8atjbd5gAsntuhbPnFpvnvo0qn.jpg */ + still_path?: string; + /** + * @default 0 + * @example 7.242 + */ + vote_average: number; + /** + * @default 0 + * @example 31 + */ + vote_count: number; + /** + * @default 0 + * @example 8 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 10 + */ + total_pages: number; + /** + * @default 0 + * @example 186 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-watchlist-movies': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /rgNzvSagnlc32TuMEBa529QFIig.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 76726 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Chronicle */ + original_title?: string; + /** @example Three high school students make an incredible discovery, leading to their developing uncanny powers beyond their understanding. As they learn to control their abilities and use them to their advantage, their lives start to spin out of control, and their darker sides begin to take over. */ + overview?: string; + /** + * @default 0 + * @example 37.148 + */ + popularity: number; + /** @example /xENglsVIIWEEhhB5lgpy33tGcKI.jpg */ + poster_path?: string; + /** @example 2012-02-01 */ + release_date?: string; + /** @example Chronicle */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 6.822 + */ + vote_average: number; + /** + * @default 0 + * @example 4741 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 34 + */ + total_pages: number; + /** + * @default 0 + * @example 677 + */ + total_results: number; + }; + }; + }; + }; + }; + 'account-watchlist-tv': { + parameters: { + query?: { + language?: string; + page?: number; + session_id?: string; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + account_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /7phlGHRupo38EnuwmkAHdNUqov3.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 58932 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example The Crazy Ones */ + original_name?: string; + /** @example The Crazy Ones is an American situation comedy series created by David E. Kelley that stars Robin Williams and Sarah Michelle Gellar. The single-camera project premiered on CBS on September 26, 2013, as part of the 2013–14 American television season as a Thursday night 9 pm entry. Bill D'Elia, Dean Lorey, Jason Winer, John Montgomery and Mark Teitelbaum serve as executive producers for 20th Century Fox Television. */ + overview?: string; + /** + * @default 0 + * @example 8.939 + */ + popularity: number; + /** @example /s2e7hTrdmNUaJDf0yDP5b4AHvrD.jpg */ + poster_path?: string; + /** @example 2013-09-26 */ + first_air_date?: string; + /** @example The Crazy Ones */ + name?: string; + /** + * @default 0 + * @example 6.176 + */ + vote_average: number; + /** + * @default 0 + * @example 94 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 17 + */ + total_pages: number; + /** + * @default 0 + * @example 325 + */ + total_results: number; + }; + }; + }; + }; + }; + 'authentication-create-guest-session': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** @example 1ce82ec1223641636ad4a60b07de3581 */ + guest_session_id?: string; + /** @example 2016-08-27 16:26:40 UTC */ + expires_at?: string; + }; + }; + }; + }; + }; + 'authentication-create-request-token': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** @example 2016-08-26 17:04:39 UTC */ + expires_at?: string; + /** @example ff5c7eeb5a8870efe3cd7fc5c282cffd26800ecd */ + request_token?: string; + }; + }; + }; + }; + }; + 'authentication-create-session': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** @example 79191836ddaa0da3df76a5ffef6f07ad6ab0c641 */ + session_id?: string; + }; + }; + }; + }; + }; + 'authentication-create-session-from-v4-token': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** @example 2629f70fb498edc263a0adb99118ac41f0053e8c */ + session_id?: string; + }; + }; + }; + }; + }; + 'authentication-create-session-from-login': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + /** @example 2018-07-24 04:10:26 UTC */ + expires_at?: string; + /** @example 1531f1a558c8357ce8990cf887ff196e8f5402ec */ + request_token?: string; + }; + }; + }; + }; + }; + 'authentication-delete-session': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example true + */ + success: boolean; + }; + }; + }; + }; + }; + 'certification-movie-list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + certifications?: { + AU?: { + /** @example E */ + certification?: string; + /** @example Exempt from classification. Films that are exempt from classification must not contain contentious material (i.e. material that would ordinarily be rated M or higher). */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + BG?: { + /** @example D */ + certification?: string; + /** @example Prohibited for persons under 16. */ + meaning?: string; + /** + * @default 0 + * @example 4 + */ + order: number; + }[]; + BR?: { + /** @example 14 */ + certification?: string; + /** @example Not recommended for minors under fourteen. More violent material, stronger sex references and/or nudity. */ + meaning?: string; + /** + * @default 0 + * @example 4 + */ + order: number; + }[]; + CA?: { + /** @example G */ + certification?: string; + /** @example All ages. */ + meaning?: string; + /** + * @default 0 + * @example 2 + */ + order: number; + }[]; + 'CA-QC'?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + DE?: { + /** @example 12 */ + certification?: string; + /** @example Children 12 or older admitted, children between 6 and 11 only when accompanied by parent or a legal guardian. */ + meaning?: string; + /** + * @default 0 + * @example 3 + */ + order: number; + }[]; + DK?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + ES?: { + /** @example A */ + certification?: string; + /** @example General admission. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + FI?: { + /** @example K-16 */ + certification?: string; + /** @example Over 16 years. */ + meaning?: string; + /** + * @default 0 + * @example 4 + */ + order: number; + }[]; + FR?: { + /** @example TP */ + certification?: string; + /** @example Valid for all audiences. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + GB?: { + /** @example 15 */ + certification?: string; + /** @example Only those over 15 years are admitted. Nobody younger than 15 can rent or buy a 15-rated VHS, DVD, Blu-ray Disc, UMD or game, or watch a film in the cinema with this rating. Films under this category can contain adult themes, hard drugs, frequent strong language and limited use of very strong language, strong violence and strong sex references, and nudity without graphic detail. Sexual activity may be portrayed but without any strong detail. Sexual violence may be shown if discreet and justified by context. */ + meaning?: string; + /** + * @default 0 + * @example 5 + */ + order: number; + }[]; + HU?: { + /** @example 6 */ + certification?: string; + /** @example Not recommended below age of 6. */ + meaning?: string; + /** + * @default 0 + * @example 2 + */ + order: number; + }[]; + IN?: { + /** @example U */ + certification?: string; + /** @example Unrestricted Public Exhibition throughout India, suitable for all age groups. Films under this category should not upset children over 4. Such films may contain educational, social or family-oriented themes. Films under this category may also contain fantasy violence and/or mild bad language. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + IT?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + LT?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + MY?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + NL?: { + /** @example AL */ + certification?: string; + /** @example All ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + NO?: { + /** @example 6 */ + certification?: string; + /** @example 6 years (no restriction for children accompanied by an adult). */ + meaning?: string; + /** + * @default 0 + * @example 2 + */ + order: number; + }[]; + NZ?: { + /** @example G */ + certification?: string; + /** @example Suitable for general audiences. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + PH?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + PT?: { + /** @example Públicos */ + certification?: string; + /** @example For all the public (especially designed for children under 3 years of age). */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + RU?: { + /** @example 6+ */ + certification?: string; + /** @example (For children above 6) – Unsuitable for children under 6. */ + meaning?: string; + /** + * @default 0 + * @example 2 + */ + order: number; + }[]; + SE?: { + /** @example 11 */ + certification?: string; + /** @example Children over the age of 7, who are accompanied by an adult, are admitted to films that have been passed for children from the age of 11. */ + meaning?: string; + /** + * @default 0 + * @example 3 + */ + order: number; + }[]; + US?: { + /** @example R */ + certification?: string; + /** @example Under 17 requires accompanying parent or adult guardian 21 or older. The parent/guardian is required to stay with the child under 17 through the entire movie, even if the parent gives the child/teenager permission to see the film alone. These films may contain strong profanity, graphic sexuality, nudity, strong violence, horror, gore, and strong drug use. A movie rated R for profanity often has more severe or frequent language than the PG-13 rating would permit. An R-rated movie may have more blood, gore, drug use, nudity, or graphic sexuality than a PG-13 movie would admit. */ + meaning?: string; + /** + * @default 0 + * @example 4 + */ + order: number; + }[]; + KR?: { + /** @example All */ + certification?: string; + /** @example Film suitable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + SK?: { + /** @example U */ + certification?: string; + /** @example General audience. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + TH?: { + /** @example P */ + certification?: string; + /** @example Educational. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + MX?: { + /** @example AA */ + certification?: string; + /** @example Informative-only rating: Understandable for children under 7 years. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + ID?: { + /** @example SU */ + certification?: string; + /** @example All ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + TR?: { + /** @example Genel İzleyici Kitlesi */ + certification?: string; + /** @example General audience. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + AR?: { + /** @example ATP */ + certification?: string; + /** @example For all public. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + GR?: { + /** @example K */ + certification?: string; + /** @example No restrictions. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + TW?: { + /** @example 0+ */ + certification?: string; + /** @example Viewing is permitted for audiences of all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + ZA?: { + /** @example A */ + certification?: string; + /** @example Suitable for all. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + SG?: { + /** @example G */ + certification?: string; + /** @example Suitable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + IE?: { + /** @example G */ + certification?: string; + /** @example Suitable for children of school going age (note: children can be enrolled in school from the age of 4). */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + PR?: { + /** @example G */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + JP?: { + /** @example G */ + certification?: string; + /** @example General, suitable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + VI?: { + /** @example G */ + certification?: string; + /** @example All ages admitted. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + CH?: { + /** @example 0 */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + IL?: { + /** @example All */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + HK?: { + /** @example I */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + MO?: { + /** @example A */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + LV?: { + /** @example U */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + LU?: { + /** @example EA */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + }; + }; + }; + }; + }; + }; + 'certifications-tv-list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + certifications?: { + AU?: { + /** @example P */ + certification?: string; + /** @example Programming is intended for younger children 2–11; commercial stations must show at least 30 minutes of P-rated content each weekday and weekends at all times. No advertisements may be shown during P-rated programs. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + BR?: { + /** @example 14 */ + certification?: string; + /** @example Content suitable for viewers over the age of 14. */ + meaning?: string; + /** + * @default 0 + * @example 3 + */ + order: number; + }[]; + CA?: { + /** @example Exempt */ + certification?: string; + /** @example Shows which are exempt from ratings (such as news and sports programming) will not display an on-screen rating at all. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + 'CA-QC'?: { + /** @example 18+ */ + certification?: string; + /** @example Only to be viewed by adults and may contain extreme violence and graphic sexual content. It is mostly used for 18+ movies and pornography. */ + meaning?: string; + /** + * @default 0 + * @example 5 + */ + order: number; + }[]; + DE?: { + /** @example 0 */ + certification?: string; + /** @example Can be aired at any time. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + ES?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + FR?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + GB?: { + /** @example U */ + certification?: string; + /** @example The U symbol stands for Universal. A U film should be suitable for audiences aged four years and over. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + HU?: { + /** @example Unrated */ + certification?: string; + /** @example Without age restriction. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + KR?: { + /** @example Exempt */ + certification?: string; + /** @example This rating is only for knowledge based game shows; lifestyle shows; documentary shows; news; current topic discussion shows; education/culture shows; sports that excludes MMA or other violent sports; and other programs that Korea Communications Standards Commission recognizes. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + LT?: { + /** @example S */ + certification?: string; + /** @example Intended for adult viewers from the age of 18 (corresponding to the age-appropriate index N-18) and broadcast between 23 (11pm) and 6 (6am) hours; Limited to minors and intended for adult audiences. */ + meaning?: string; + /** + * @default 0 + * @example 3 + */ + order: number; + }[]; + NL?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + PH?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + PT?: { + /** @example 12AP */ + certification?: string; + /** @example Acompanhamento Parental (may not be suitable for children under 12, parental guidance advised). */ + meaning?: string; + /** + * @default 0 + * @example 3 + */ + order: number; + }[]; + RU?: { + /** @example 16+ */ + certification?: string; + /** @example Only teens the age of 16 or older can watch. */ + meaning?: string; + /** + * @default 0 + * @example 4 + */ + order: number; + }[]; + SK?: { + /** @example NR */ + certification?: string; + /** @example No rating information. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + TH?: { + /** @example ส */ + certification?: string; + /** @example Sor - Educational movies which the public should be encouraged to see. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + US?: { + /** @example TV-MA */ + certification?: string; + /** @example This program is specifically designed to be viewed by adults and therefore may be unsuitable for children under 17. */ + meaning?: string; + /** + * @default 0 + * @example 6 + */ + order: number; + }[]; + IT?: { + /** @example T */ + certification?: string; + /** @example All ages admitted. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + FI?: { + /** @example S */ + certification?: string; + /** @example Allowed at all times. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + MY?: { + /** @example U */ + certification?: string; + /** @example No age limit. Can be broadcast anytime. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + NZ?: { + /** @example G */ + certification?: string; + /** @example Approved for general viewing. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + NO?: { + /** @example A */ + certification?: string; + /** @example Allowed at all times. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + BG?: { + /** @example Unrated */ + certification?: string; + /** @example Can be viewed for each age. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + MX?: { + /** @example AA */ + certification?: string; + /** @example Aimed at children (can be broadcast anytime). */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + IN?: { + /** @example U */ + certification?: string; + /** @example Viewable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + DK?: { + /** @example A */ + certification?: string; + /** @example Suitable for a general audience. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + SE?: { + /** @example Btl */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + ID?: { + /** @example SU */ + certification?: string; + /** @example Suitable for general audiences over the age of 2 years. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + TR?: { + /** @example Genel İzleyici */ + certification?: string; + /** @example General audience. Suitable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + AR?: { + /** @example ATP */ + certification?: string; + /** @example Suitable for all audiences. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + PL?: { + /** @example 0 */ + certification?: string; + /** @example Positive or neutral view of the world, little to no violence, non-sexual love, and no sexual content. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + MA?: { + /** @example NR */ + certification?: string; + /** @example All audiences. */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + GR?: { + /** @example K */ + certification?: string; + /** @example Suitable for all ages. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + IL?: { + /** @example E */ + certification?: string; + /** @example Exempt from classification. This rating is usually applied to live broadcasts. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + TW?: { + /** @example 0+ */ + certification?: string; + /** @example Suitable for watching by general audiences. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + ZA?: { + /** @example All */ + certification?: string; + /** @example This is a programme/film that does not contain any obscenity, and is suitable for family viewing. A logo must be displayed in the corner of the screen for 30 seconds after each commercial break. */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + SG?: { + /** @example G */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + }[]; + PR?: { + /** @example NR */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + VI?: { + /** @example NR */ + certification?: string; + /** @example */ + meaning?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + }; + }; + }; + }; + }; + }; + 'changes-movie-list': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + /** + * @default 0 + * @example 1120293 + */ + id: number; + /** + * @default true + * @example false + */ + adult: boolean; + }[]; + /** + * @default 0 + * @example 3 + */ + page: number; + /** + * @default 0 + * @example 57 + */ + total_pages: number; + /** + * @default 0 + * @example 5700 + */ + total_results: number; + }; + }; + }; + }; + }; + 'changes-people-list': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + /** + * @default 0 + * @example 4037513 + */ + id: number; + /** + * @default true + * @example false + */ + adult: boolean; + }[]; + /** + * @default 0 + * @example 1 + */ + page: number; + /** + * @default 0 + * @example 53 + */ + total_pages: number; + /** + * @default 0 + * @example 5292 + */ + total_results: number; + }; + }; + }; + }; + }; + 'changes-tv-list': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + /** + * @default 0 + * @example 225591 + */ + id: number; + /** + * @default true + * @example false + */ + adult: boolean; + }[]; + /** + * @default 0 + * @example 1 + */ + page: number; + /** + * @default 0 + * @example 18 + */ + total_pages: number; + /** + * @default 0 + * @example 1763 + */ + total_results: number; + }; + }; + }; + }; + }; + 'collection-details': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + collection_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 10 + */ + id: number; + /** @example Star Wars Collection */ + name?: string; + /** @example en */ + original_language?: string; + /** @example Star Wars Collection */ + original_name?: string; + /** @example An epic space-opera theatrical film series, which depicts the adventures of various characters "a long time ago in a galaxy far, far away…." */ + overview?: string; + /** @example /22dj38IckjzEEUZwN1tPU5VJ1qq.jpg */ + poster_path?: string; + /** @example /4z9ijhgEthfRHShoOvMaBlpciXS.jpg */ + backdrop_path?: string; + parts?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /2w4xG178RpB4MDAIfTkqAuSJzec.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 11 + */ + id: number; + /** @example Star Wars */ + name?: string; + /** @example Star Wars */ + original_name?: string; + /** @example Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire. */ + overview?: string; + /** @example /6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + /** @example en */ + original_language?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 15.8557 + */ + popularity: number; + /** @example 1977-05-25 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.205 + */ + vote_average: number; + /** + * @default 0 + * @example 21522 + */ + vote_count: number; + }[]; + }; + }; + }; + }; + }; + 'collection-images': { + parameters: { + query?: { + /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ + include_image_language?: string; + language?: string; + }; + header?: never; + path: { + collection_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 10 + */ + id: number; + backdrops?: { + /** + * @default 0 + * @example 1.778 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 1080 + */ + height: number; + iso_639_1?: unknown; + /** @example /d8duYyyC9J5T825Hg7grmaabfxQ.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.464 + */ + vote_average: number; + /** + * @default 0 + * @example 30 + */ + vote_count: number; + /** + * @default 0 + * @example 1920 + */ + width: number; + }[]; + posters?: { + /** + * @default 0 + * @example 0.667 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 3000 + */ + height: number; + /** @example en */ + iso_639_1?: string; + /** @example /r8Ph5MYXL04Qzu4QBbq2KjqwtkQ.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.516 + */ + vote_average: number; + /** + * @default 0 + * @example 14 + */ + vote_count: number; + /** + * @default 0 + * @example 2000 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'collection-translations': { + parameters: { + query?: never; + header?: never; + path: { + collection_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 10 + */ + id: number; + translations?: { + /** @example AE */ + iso_3166_1?: string; + /** @example ar */ + iso_639_1?: string; + /** @example العربية */ + name?: string; + /** @example Arabic */ + english_name?: string; + data?: { + /** @example */ + title?: string; + /** @example */ + overview?: string; + /** @example */ + homepage?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'company-details': { + parameters: { + query?: never; + header?: never; + path: { + company_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example */ + description?: string; + /** @example San Francisco, California */ + headquarters?: string; + /** @example https://www.lucasfilm.com */ + homepage?: string; + /** + * @default 0 + * @example 1 + */ + id: number; + /** @example /o86DbpburjxrqAzEDhXZcyE8pDb.png */ + logo_path?: string; + /** @example Lucasfilm Ltd. */ + name?: string; + /** @example US */ + origin_country?: string; + parent_company?: unknown; + }; + }; + }; + }; + }; + 'company-alternative-names': { + parameters: { + query?: never; + header?: never; + path: { + company_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + id: number; + results?: { + /** @example 루카스필름 */ + name?: string; + /** @example */ + type?: string; + }[]; + }; + }; + }; + }; + }; + 'company-images': { + parameters: { + query?: never; + header?: never; + path: { + company_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + id: number; + logos?: { + /** + * @default 0 + * @example 2.97979797979798 + */ + aspect_ratio: number; + /** @example /o86DbpburjxrqAzEDhXZcyE8pDb.png */ + file_path?: string; + /** + * @default 0 + * @example 99 + */ + height: number; + /** @example 5aa080d6c3a3683fea00011e */ + id?: string; + /** @example .svg */ + file_type?: string; + /** + * @default 0 + * @example 5.384 + */ + vote_average: number; + /** + * @default 0 + * @example 2 + */ + vote_count: number; + /** + * @default 0 + * @example 295 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'configuration-details': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + images?: { + /** @example http://image.tmdb.org/t/p/ */ + base_url?: string; + /** @example https://image.tmdb.org/t/p/ */ + secure_base_url?: string; + backdrop_sizes?: string[]; + logo_sizes?: string[]; + poster_sizes?: string[]; + profile_sizes?: string[]; + still_sizes?: string[]; + }; + change_keys?: string[]; + }; + }; + }; + }; + }; + 'configuration-countries': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example AD */ + iso_3166_1?: string; + /** @example Andorra */ + english_name?: string; + /** @example Andorra */ + native_name?: string; + }[]; + }; + }; + }; + }; + 'configuration-jobs': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example Production */ + department?: string; + jobs?: string[]; + }[]; + }; + }; + }; + }; + 'configuration-languages': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example bi */ + iso_639_1?: string; + /** @example Bislama */ + english_name?: string; + /** @example */ + name?: string; + }[]; + }; + }; + }; + }; + 'configuration-primary-translations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': string[]; + }; + }; + }; + }; + 'configuration-timezones': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example AD */ + iso_3166_1?: string; + zones?: string[]; + }[]; + }; + }; + }; + }; + 'credit-details': { + parameters: { + query?: never; + header?: never; + path: { + credit_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example cast */ + credit_type?: string; + /** @example Acting */ + department?: string; + /** @example Actor */ + job?: string; + media?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /uDgy6hyPd82kOHh6I95FLtLnj6p.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 100088 + */ + id: number; + /** @example The Last of Us */ + name?: string; + /** @example en */ + original_language?: string; + /** @example The Last of Us */ + original_name?: string; + /** @example Zwanzig Jahre nachdem die moderne Zivilisation zerstört wurde. – Joel, ein abgehärteter Überlebender, wird angeheuert, um Ellie, ein 14-jähriges Mädchen, aus einer bedrückenden Quarantänezone zu schmuggeln. Was als kleiner Job beginnt, wird bald zu einer brutalen, herzzerreißenden Reise, bei der die beiden die USA durchqueren müssen und aufeinander angewiesen sind, um zu überleben. */ + overview?: string; + /** @example /igwIPNClQpGVzb61QlGqcpT5zUy.jpg */ + poster_path?: string; + /** @example tv */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 898.378 + */ + popularity: number; + /** @example 2023-01-15 */ + first_air_date?: string; + /** + * @default 0 + * @example 8.749 + */ + vote_average: number; + /** + * @default 0 + * @example 3341 + */ + vote_count: number; + origin_country?: string[]; + /** @example Joel Miller */ + character?: string; + episodes?: unknown[]; + seasons?: { + /** @example 2023-01-15 */ + air_date?: string; + /** + * @default 0 + * @example 9 + */ + episode_count: number; + /** + * @default 0 + * @example 144593 + */ + id: number; + /** @example Staffel 1 */ + name?: string; + /** @example Die 1. Staffel der Endzeit-Horrorserie The Last of Us feierte ihre Premiere am 15. Januar 2023 bei HBO. In Staffel 1 beginnt für den Überlebenden Joel und das Mädchen Ellie eine Reise durch das postapokalyptische Amerika, in dem Plünderer und mutierte Wesen ihnen nach dem Leben trachten. */ + overview?: string; + /** @example /aUQKIpZZ31KWbpdHMCmaV76u78T.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** + * @default 0 + * @example 100088 + */ + show_id: number; + }[]; + }; + /** @example tv */ + media_type?: string; + /** @example 6024a814c0ae36003d59cc3c */ + id?: string; + person?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 1253360 + */ + id: number; + /** @example Pedro Pascal */ + name?: string; + /** @example Pedro Pascal */ + original_name?: string; + /** @example person */ + media_type?: string; + /** + * @default 0 + * @example 106.095 + */ + popularity: number; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** @example Acting */ + known_for_department?: string; + /** @example /dBOrm29cr7NUrjiDQMTtrTyDpfy.jpg */ + profile_path?: string; + }; + }; + }; + }; + }; + }; + 'discover-movie': { + parameters: { + query?: { + /** @description use in conjunction with `region` */ + certification?: string; + /** @description use in conjunction with `region` */ + 'certification.gte'?: string; + /** @description use in conjunction with `region` */ + 'certification.lte'?: string; + /** @description use in conjunction with the `certification`, `certification.gte` and `certification.lte` filters */ + certification_country?: string; + include_adult?: boolean; + include_video?: boolean; + language?: string; + page?: number; + primary_release_year?: number; + 'primary_release_date.gte'?: string; + 'primary_release_date.lte'?: string; + region?: string; + 'release_date.gte'?: string; + 'release_date.lte'?: string; + sort_by?: + | 'original_title.asc' + | 'original_title.desc' + | 'popularity.asc' + | 'popularity.desc' + | 'revenue.asc' + | 'revenue.desc' + | 'primary_release_date.asc' + | 'title.asc' + | 'title.desc' + | 'primary_release_date.desc' + | 'vote_average.asc' + | 'vote_average.desc' + | 'vote_count.asc' + | 'vote_count.desc'; + 'vote_average.gte'?: number; + 'vote_average.lte'?: number; + 'vote_count.gte'?: number; + 'vote_count.lte'?: number; + /** @description use in conjunction with `with_watch_monetization_types ` or `with_watch_providers ` */ + watch_region?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_cast?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_companies?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_crew?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_genres?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_keywords?: string; + with_origin_country?: string; + with_original_language?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_people?: string; + /** @description possible values are: [1, 2, 3, 4, 5, 6] can be a comma (`AND`) or pipe (`OR`) separated query, can be used in conjunction with `region` */ + with_release_type?: number; + 'with_runtime.gte'?: number; + 'with_runtime.lte'?: number; + /** @description possible values are: [flatrate, free, ads, rent, buy] use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ + with_watch_monetization_types?: string; + /** @description use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ + with_watch_providers?: string; + without_companies?: string; + without_genres?: string; + without_keywords?: string; + without_watch_providers?: string; + year?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /8YFL5QQVPy3AgrEQxNYVSgiPEbe.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 640146 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + original_title?: string; + /** @example Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible. */ + overview?: string; + /** + * @default 0 + * @example 9272.643 + */ + popularity: number; + /** @example /ngl2FKBlU4fhbdsrtdom9LVLBXw.jpg */ + poster_path?: string; + /** @example 2023-02-15 */ + release_date?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 6.5 + */ + vote_average: number; + /** + * @default 0 + * @example 1856 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 38020 + */ + total_pages: number; + /** + * @default 0 + * @example 760385 + */ + total_results: number; + }; + }; + }; + }; + }; + 'discover-tv': { + parameters: { + query?: { + 'air_date.gte'?: string; + 'air_date.lte'?: string; + first_air_date_year?: number; + 'first_air_date.gte'?: string; + 'first_air_date.lte'?: string; + include_adult?: boolean; + include_null_first_air_dates?: boolean; + language?: string; + page?: number; + screened_theatrically?: boolean; + sort_by?: + | 'first_air_date.asc' + | 'first_air_date.desc' + | 'name.asc' + | 'name.desc' + | 'original_name.asc' + | 'original_name.desc' + | 'popularity.asc' + | 'popularity.desc' + | 'vote_average.asc' + | 'vote_average.desc' + | 'vote_count.asc' + | 'vote_count.desc'; + timezone?: string; + 'vote_average.gte'?: number; + 'vote_average.lte'?: number; + 'vote_count.gte'?: number; + 'vote_count.lte'?: number; + /** @description use in conjunction with `with_watch_monetization_types ` or `with_watch_providers ` */ + watch_region?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_companies?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_genres?: string; + /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ + with_keywords?: string; + with_networks?: number; + with_origin_country?: string; + with_original_language?: string; + 'with_runtime.gte'?: number; + 'with_runtime.lte'?: number; + /** @description possible values are: [0, 1, 2, 3, 4, 5], can be a comma (`AND`) or pipe (`OR`) separated query */ + with_status?: string; + /** @description possible values are: [flatrate, free, ads, rent, buy] use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ + with_watch_monetization_types?: string; + /** @description use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ + with_watch_providers?: string; + without_companies?: string; + without_genres?: string; + without_keywords?: string; + without_watch_providers?: string; + /** @description possible values are: [0, 1, 2, 3, 4, 5, 6], can be a comma (`AND`) or pipe (`OR`) separated query */ + with_type?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ + backdrop_path?: string; + /** @example 2023-01-23 */ + first_air_date?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 202250 + */ + id: number; + /** @example Dirty Linen */ + name?: string; + origin_country?: string[]; + /** @example tl */ + original_language?: string; + /** @example Dirty Linen */ + original_name?: string; + /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ + overview?: string; + /** + * @default 0 + * @example 2684.061 + */ + popularity: number; + /** @example /ujlkQtHAVShWyWTloGU2Vh5Jbo9.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 5 + */ + vote_average: number; + /** + * @default 0 + * @example 13 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 7414 + */ + total_pages: number; + /** + * @default 0 + * @example 148265 + */ + total_results: number; + }; + }; + }; + }; + }; + 'find-by-id': { + parameters: { + query: { + external_source: '' | 'imdb_id' | 'facebook_id' | 'instagram_id' | 'tvdb_id' | 'tiktok_id' | 'twitter_id' | 'wikidata_id' | 'youtube_id'; + language?: string; + }; + header?: never; + path: { + external_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + movie_results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 934433 + */ + id: number; + /** @example Scream VI */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Scream VI */ + original_title?: string; + /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ + overview?: string; + /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 853.917 + */ + popularity: number; + /** @example 2023-03-08 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.388 + */ + vote_average: number; + /** + * @default 0 + * @example 708 + */ + vote_count: number; + }[]; + person_results?: unknown[]; + tv_results?: unknown[]; + tv_episode_results?: unknown[]; + tv_season_results?: unknown[]; + }; + }; + }; + }; + }; + 'genre-movie-list': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + genres?: { + /** + * @default 0 + * @example 28 + */ + id: number; + /** @example Action */ + name?: string; + }[]; + }; + }; + }; + }; + }; + 'genre-tv-list': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + genres?: { + /** + * @default 0 + * @example 10759 + */ + id: number; + /** @example Action & Adventure */ + name?: string; + }[]; + }; + }; + }; + }; + }; + 'guest-session-rated-movies': { + parameters: { + query?: { + language?: string; + page?: number; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + guest_session_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /ikR2qy9xJCHX7M8i5rcvuNfdYXs.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 16 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Dancer in the Dark */ + original_title?: string; + /** @example Selma, a Czech immigrant on the verge of blindness, struggles to make ends meet for herself and her son, who has inherited the same genetic disorder and will suffer the same fate without an expensive operation. When life gets too difficult, Selma learns to cope through her love of musicals, escaping life's troubles - even if just for a moment - by dreaming up little numbers to the rhythmic beats of her surroundings. */ + overview?: string; + /** + * @default 0 + * @example 14.684 + */ + popularity: number; + /** @example /8Wdd3fQfbbQeoSfWpHrDfaFNhBU.jpg */ + poster_path?: string; + /** @example 2000-06-30 */ + release_date?: string; + /** @example Dancer in the Dark */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.885 + */ + vote_average: number; + /** + * @default 0 + * @example 1549 + */ + vote_count: number; + /** + * @default 0 + * @example 8.5 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'guest-session-rated-tv': { + parameters: { + query?: { + language?: string; + page?: number; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + guest_session_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /2OMB0ynKlyIenMJWI2Dy9IWT4c.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1399 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Game of Thrones */ + original_name?: string; + /** @example Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond. */ + overview?: string; + /** + * @default 0 + * @example 404.299 + */ + popularity: number; + /** @example /7WUHnWGx5OO145IRxPDUkQSh4C7.jpg */ + poster_path?: string; + /** @example 2011-04-17 */ + first_air_date?: string; + /** @example Game of Thrones */ + name?: string; + /** + * @default 0 + * @example 8.436 + */ + vote_average: number; + /** + * @default 0 + * @example 21025 + */ + vote_count: number; + /** + * @default 0 + * @example 8.5 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'guest-session-rated-tv-episodes': { + parameters: { + query?: { + language?: string; + page?: number; + sort_by?: 'created_at.asc' | 'created_at.desc'; + }; + header?: never; + path: { + guest_session_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example 2011-04-17 */ + air_date?: string; + /** + * @default 0 + * @example 1 + */ + episode_number: number; + /** + * @default 0 + * @example 63056 + */ + id: number; + /** @example Winter Is Coming */ + name?: string; + /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ + overview?: string; + /** @example 101 */ + production_code?: string; + /** + * @default 0 + * @example 62 + */ + runtime: number; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** + * @default 0 + * @example 1399 + */ + show_id: number; + /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ + still_path?: string; + /** + * @default 0 + * @example 7.843 + */ + vote_average: number; + /** + * @default 0 + * @example 286 + */ + vote_count: number; + /** + * @default 0 + * @example 8.5 + */ + rating: number; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'keyword-details': { + parameters: { + query?: never; + header?: never; + path: { + keyword_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1701 + */ + id: number; + /** @example hero */ + name?: string; + }; + }; + }; + }; + }; + 'keyword-movies': { + parameters: { + query?: { + include_adult?: boolean; + language?: string; + page?: number; + }; + header?: never; + path: { + keyword_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1701 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /3CxUndGhUcZdt1Zggjdb2HkLLQX.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 640146 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + original_title?: string; + /** @example Das Superhelden-Duo Scott Lang und Hope Van Dyne erkundet zusammen mit Hopes Eltern Hank Pym und Janet Van Dyne das Quantenreich, interagiert mit seltsamen neuen Kreaturen und begibt sich auf ein Abenteuer, das sie über die Grenzen dessen hinaustreiben wird, was sie für möglich gehalten haben. */ + overview?: string; + /** + * @default 0 + * @example 9200.005 + */ + popularity: number; + /** @example /nA5otwVxAfpBP4PVgeuBk3qHcLY.jpg */ + poster_path?: string; + /** @example 2023-02-15 */ + release_date?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 6.5 + */ + vote_average: number; + /** + * @default 0 + * @example 2079 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 11 + */ + total_pages: number; + /** + * @default 0 + * @example 211 + */ + total_results: number; + }; + }; + }; + }; + }; + 'list-add-movie': { + parameters: { + query: { + session_id: string; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY?: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 12 + */ + status_code: number; + /** @example The item/record was updated successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'list-check-item-status': { + parameters: { + query?: { + language?: string; + movie_id?: number; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + id: number; + /** + * @default true + * @example true + */ + item_present: boolean; + }; + }; + }; + }; + }; + 'list-clear': { + parameters: { + query: { + session_id: string; + confirm: boolean; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 12 + */ + status_code: number; + /** @example The item/record was updated successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'list-create': { + parameters: { + query: { + session_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example The item/record was created successfully. */ + status_message?: string; + /** + * @default true + * @example true + */ + success: boolean; + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** + * @default 0 + * @example 5861 + */ + list_id: number; + }; + }; + }; + }; + }; + 'list-details': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example travisbell */ + created_by?: string; + /** @example The idea behind this list is to collect the live action comic book movies from within the Marvel franchise. */ + description?: string; + /** + * @default 0 + * @example 0 + */ + favorite_count: number; + /** @example 1 */ + id?: string; + items?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /14QbnygCuTO0vl7CAFmPf1fgZfV.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 634649 + */ + id: number; + /** @example movie */ + media_type?: string; + /** @example en */ + original_language?: string; + /** @example Spider-Man: No Way Home */ + original_title?: string; + /** @example Peter Parker ist demaskiert und kann sein normales Leben nicht mehr von den hohen Einsätzen als Superheld trennen. Als er Doctor Strange um Hilfe bittet, wird die Lage noch gefährlicher und er muss entdecken, was es wirklich bedeutet, Spider-Man zu sein. */ + overview?: string; + /** + * @default 0 + * @example 398.217 + */ + popularity: number; + /** @example /iNKf4D0AzOj9GLq8ZyG3WZaqibL.jpg */ + poster_path?: string; + /** @example 2021-12-15 */ + release_date?: string; + /** @example Spider-Man: No Way Home */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8 + */ + vote_average: number; + /** + * @default 0 + * @example 17267 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 59 + */ + item_count: number; + /** @example en */ + iso_639_1?: string; + /** @example The Marvel Universe */ + name?: string; + /** @example /coJVIUEOToAEGViuhclM7pXC75R.jpg */ + poster_path?: string; + }; + }; + }; + }; + }; + 'list-delete': { + parameters: { + query: { + session_id: string; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 12 + */ + status_code: number; + /** @example The item/record was updated successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'list-remove-movie': { + parameters: { + query: { + session_id: string; + }; + header?: never; + path: { + list_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 13 + */ + status_code: number; + /** @example The item/record was deleted successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'movie-now-playing-list': { + parameters: { + query?: { + language?: string; + page?: number; + /** @description ISO-3166-1 code */ + region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dates?: { + /** @example 2023-05-03 */ + maximum?: string; + /** @example 2023-03-16 */ + minimum?: string; + }; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /iJQIbOPm81fPEGKt5BPuZmfnA54.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 502356 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example The Super Mario Bros. Movie */ + original_title?: string; + /** @example While working underground to fix a water main, Brooklyn plumbers—and brothers—Mario and Luigi are transported down a mysterious pipe and wander into a magical new world. But when the brothers are separated, Mario embarks on an epic quest to find Luigi. */ + overview?: string; + /** + * @default 0 + * @example 6572.614 + */ + popularity: number; + /** @example /qNBAXBIQlnOThrVvA6mA2B5ggV6.jpg */ + poster_path?: string; + /** @example 2023-04-05 */ + release_date?: string; + /** @example The Super Mario Bros. Movie */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.5 + */ + vote_average: number; + /** + * @default 0 + * @example 1456 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 87 + */ + total_pages: number; + /** + * @default 0 + * @example 1734 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-popular-list': { + parameters: { + query?: { + language?: string; + page?: number; + /** @description ISO-3166-1 code */ + region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /gMJngTNfaqCSCqGD4y8lVMZXKDn.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 640146 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + original_title?: string; + /** @example Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible. */ + overview?: string; + /** + * @default 0 + * @example 8567.865 + */ + popularity: number; + /** @example /ngl2FKBlU4fhbdsrtdom9LVLBXw.jpg */ + poster_path?: string; + /** @example 2023-02-15 */ + release_date?: string; + /** @example Ant-Man and the Wasp: Quantumania */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 6.5 + */ + vote_average: number; + /** + * @default 0 + * @example 1886 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 38029 + */ + total_pages: number; + /** + * @default 0 + * @example 760569 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-top-rated-list': { + parameters: { + query?: { + language?: string; + page?: number; + /** @description ISO-3166-1 code */ + region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /tmU7GeKVybMWFButWEGl2M4GeiP.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 238 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example The Godfather */ + original_title?: string; + /** @example Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge. */ + overview?: string; + /** + * @default 0 + * @example 100.932 + */ + popularity: number; + /** @example /3bhkrj58Vtu7enYsRolD1fZdja1.jpg */ + poster_path?: string; + /** @example 1972-03-14 */ + release_date?: string; + /** @example The Godfather */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.7 + */ + vote_average: number; + /** + * @default 0 + * @example 17806 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 552 + */ + total_pages: number; + /** + * @default 0 + * @example 11032 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-upcoming-list': { + parameters: { + query?: { + language?: string; + page?: number; + /** @description ISO-3166-1 code */ + region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dates?: { + /** @example 2023-05-23 */ + maximum?: string; + /** @example 2023-05-04 */ + minimum?: string; + }; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /7bWxAsNPv9CXHOhZbJVlj2KxgfP.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 713704 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Evil Dead Rise */ + original_title?: string; + /** @example Two sisters find an ancient vinyl that gives birth to bloodthirsty demons that run amok in a Los Angeles apartment building and thrusts them into a primal battle for survival as they face the most nightmarish version of family imaginable. */ + overview?: string; + /** + * @default 0 + * @example 1696.367 + */ + popularity: number; + /** @example /mIBCtPvKZQlxubxKMeViO2UrP3q.jpg */ + poster_path?: string; + /** @example 2023-04-12 */ + release_date?: string; + /** @example Evil Dead Rise */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7 + */ + vote_average: number; + /** + * @default 0 + * @example 207 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 19 + */ + total_pages: number; + /** + * @default 0 + * @example 369 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-details': { + parameters: { + query?: { + /** @description comma separated list of endpoints within this namespace, 20 items max */ + append_to_response?: string; + language?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ + backdrop_path?: string; + belongs_to_collection?: unknown; + /** + * @default 0 + * @example 63000000 + */ + budget: number; + genres?: { + /** + * @default 0 + * @example 18 + */ + id: number; + /** @example Drama */ + name?: string; + }[]; + /** @example http://www.foxmovies.com/movies/fight-club */ + homepage?: string; + /** + * @default 0 + * @example 550 + */ + id: number; + /** @example tt0137523 */ + imdb_id?: string; + /** @example en */ + original_language?: string; + /** @example Fight Club */ + original_title?: string; + /** @example A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion. */ + overview?: string; + /** + * @default 0 + * @example 61.416 + */ + popularity: number; + /** @example /pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg */ + poster_path?: string; + production_companies?: { + /** + * @default 0 + * @example 508 + */ + id: number; + /** @example /7cxRWzi4LsVm4Utfpr1hfARNurT.png */ + logo_path?: string; + /** @example Regency Enterprises */ + name?: string; + /** @example US */ + origin_country?: string; + }[]; + production_countries?: { + /** @example US */ + iso_3166_1?: string; + /** @example United States of America */ + name?: string; + }[]; + /** @example 1999-10-15 */ + release_date?: string; + /** + * @default 0 + * @example 100853753 + */ + revenue: number; + /** + * @default 0 + * @example 139 + */ + runtime: number; + spoken_languages?: { + /** @example English */ + english_name?: string; + /** @example en */ + iso_639_1?: string; + /** @example English */ + name?: string; + }[]; + /** @example Released */ + status?: string; + /** @example Mischief. Mayhem. Soap. */ + tagline?: string; + /** @example Fight Club */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.433 + */ + vote_average: number; + /** + * @default 0 + * @example 26280 + */ + vote_count: number; + }; + }; + }; + }; + }; + 'movie-account-states': { + parameters: { + query?: { + session_id?: string; + guest_session_id?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** + * @default true + * @example true + */ + favorite: boolean; + rated?: { + /** + * @default 0 + * @example 9 + */ + value: number; + }; + /** + * @default true + * @example false + */ + watchlist: boolean; + }; + }; + }; + }; + }; + 'movie-alternative-titles': { + parameters: { + query?: { + /** @description specify a ISO-3166-1 value to filter the results */ + country?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + titles?: { + /** @example RS */ + iso_3166_1?: string; + /** @example Borilački klub */ + title?: string; + /** @example */ + type?: string; + }[]; + }; + }; + }; + }; + }; + 'movie-changes': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + changes?: { + /** @example images */ + key?: string; + items?: { + /** @example 643197b96dea3a00d4377270 */ + id?: string; + /** @example added */ + action?: string; + /** @example 2023-04-08 16:35:05 UTC */ + time?: string; + /** @example */ + iso_639_1?: string; + /** @example */ + iso_3166_1?: string; + value?: { + poster?: { + /** @example /s9ZrHprviFCx3azfWNBtt1LPSnL.jpg */ + file_path?: string; + }; + }; + }[]; + }[]; + }; + }; + }; + }; + }; + 'movie-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 819 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Edward Norton */ + name?: string; + /** @example Edward Norton */ + original_name?: string; + /** + * @default 0 + * @example 26.99 + */ + popularity: number; + /** @example /8nytsqL59SFJTVYVrN72k6qkGgJ.jpg */ + profile_path?: string; + /** + * @default 0 + * @example 4 + */ + cast_id: number; + /** @example The Narrator */ + character?: string; + /** @example 52fe4250c3a36847f80149f3 */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 376 + */ + id: number; + /** @example Production */ + known_for_department?: string; + /** @example Arnon Milchan */ + name?: string; + /** @example Arnon Milchan */ + original_name?: string; + /** + * @default 0 + * @example 2.931 + */ + popularity: number; + /** @example /b2hBExX4NnczNAnLuTBF4kmNhZm.jpg */ + profile_path?: string; + /** @example 55731b8192514111610027d7 */ + credit_id?: string; + /** @example Production */ + department?: string; + /** @example Executive Producer */ + job?: string; + }[]; + }; + }; + }; + }; + }; + 'movie-external-ids': { + parameters: { + query?: never; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** @example tt0137523 */ + imdb_id?: string; + wikidata_id?: unknown; + /** @example FightClub */ + facebook_id?: string; + instagram_id?: unknown; + twitter_id?: unknown; + }; + }; + }; + }; + }; + 'movie-images': { + parameters: { + query?: { + /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ + include_image_language?: string; + language?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + backdrops?: { + /** + * @default 0 + * @example 1.778 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 800 + */ + height: number; + iso_639_1?: unknown; + /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.622 + */ + vote_average: number; + /** + * @default 0 + * @example 20 + */ + vote_count: number; + /** + * @default 0 + * @example 1422 + */ + width: number; + }[]; + /** + * @default 0 + * @example 550 + */ + id: number; + logos?: { + /** + * @default 0 + * @example 5.203 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 79 + */ + height: number; + /** @example he */ + iso_639_1?: string; + /** @example /c1KLulrIhUqY5fT42nmC5aERGCp.png */ + file_path?: string; + /** + * @default 0 + * @example 5.312 + */ + vote_average: number; + /** + * @default 0 + * @example 1 + */ + vote_count: number; + /** + * @default 0 + * @example 411 + */ + width: number; + }[]; + posters?: { + /** + * @default 0 + * @example 0.667 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 900 + */ + height: number; + /** @example pt */ + iso_639_1?: string; + /** @example /r3pPehX4ik8NLYPpbDRAh0YRtMb.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.258 + */ + vote_average: number; + /** + * @default 0 + * @example 6 + */ + vote_count: number; + /** + * @default 0 + * @example 600 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'movie-keywords': { + parameters: { + query?: never; + header?: never; + path: { + movie_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + keywords?: { + /** + * @default 0 + * @example 818 + */ + id: number; + /** @example based on novel or book */ + name?: string; + }[]; + }; + }; + }; + }; + }; + 'movie-latest-id': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + backdrop_path?: unknown; + belongs_to_collection?: unknown; + /** + * @default 0 + * @example 0 + */ + budget: number; + genres?: unknown[]; + /** @example */ + homepage?: string; + /** + * @default 0 + * @example 1119232 + */ + id: number; + imdb_id?: unknown; + /** @example fr */ + original_language?: string; + /** @example König Charles III */ + original_title?: string; + /** @example */ + overview?: string; + /** + * @default 0 + * @example 0 + */ + popularity: number; + poster_path?: unknown; + production_companies?: unknown[]; + production_countries?: unknown[]; + /** @example */ + release_date?: string; + /** + * @default 0 + * @example 0 + */ + revenue: number; + /** + * @default 0 + * @example 0 + */ + runtime: number; + spoken_languages?: unknown[]; + /** @example Released */ + status?: string; + /** @example */ + tagline?: string; + /** @example König Charles III */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 0 + */ + vote_average: number; + /** + * @default 0 + * @example 0 + */ + vote_count: number; + }; + }; + }; + }; + }; + 'movie-lists': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example Movies I own */ + description?: string; + /** + * @default 0 + * @example 0 + */ + favorite_count: number; + /** + * @default 0 + * @example 8248696 + */ + id: number; + /** + * @default 0 + * @example 409 + */ + item_count: number; + /** @example en */ + iso_639_1?: string; + /** @example movie */ + list_type?: string; + /** @example My Movies */ + name?: string; + poster_path?: unknown; + }[]; + /** + * @default 0 + * @example 122 + */ + total_pages: number; + /** + * @default 0 + * @example 2422 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-recommendations': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': Record; + }; + }; + }; + }; + 'movie-release-dates': { + parameters: { + query?: never; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + results?: { + /** @example BG */ + iso_3166_1?: string; + release_dates?: { + /** @example c */ + certification?: string; + descriptors?: unknown[]; + /** @example */ + iso_639_1?: string; + /** @example */ + note?: string; + /** @example 2012-08-28T00:00:00.000Z */ + release_date?: string; + /** + * @default 0 + * @example 3 + */ + type: number; + }[]; + }[]; + }; + }; + }; + }; + }; + 'movie-reviews': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example Goddard */ + author?: string; + author_details?: { + /** @example */ + name?: string; + /** @example Goddard */ + username?: string; + /** @example /https://secure.gravatar.com/avatar/f248ec34f953bc62cafcbdd81fddd6b6.jpg */ + avatar_path?: string; + rating?: unknown; + }; + /** @example Pretty awesome movie. It shows what one crazy person can convince other crazy people to do. Everyone needs something to believe in. I recommend Jesus Christ, but they want Tyler Durden. */ + content?: string; + /** @example 2018-06-09T17:51:53.359Z */ + created_at?: string; + /** @example 5b1c13b9c3a36848f2026384 */ + id?: string; + /** @example 2021-06-23T15:58:09.421Z */ + updated_at?: string; + /** @example https://www.themoviedb.org/review/5b1c13b9c3a36848f2026384 */ + url?: string; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 8 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-similar': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /3YAldML4EDyoC6RBpzceALigrAZ.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 9300 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Orlando */ + original_title?: string; + /** @example England, 1600. Queen Elizabeth I promises Orlando, a young nobleman obsessed with poetry, that she will grant him land and fortune if he agrees to satisfy a very particular request. */ + overview?: string; + /** + * @default 0 + * @example 7.768 + */ + popularity: number; + /** @example /xvz0qZkXXMq3dH2Revxii8drxWc.jpg */ + poster_path?: string; + /** @example 1992-12-11 */ + release_date?: string; + /** @example Orlando */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 6.966 + */ + vote_average: number; + /** + * @default 0 + * @example 262 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 364 + */ + total_pages: number; + /** + * @default 0 + * @example 7269 + */ + total_results: number; + }; + }; + }; + }; + }; + 'movie-translations': { + parameters: { + query?: never; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + translations?: { + /** @example SA */ + iso_3166_1?: string; + /** @example ar */ + iso_639_1?: string; + /** @example العربية */ + name?: string; + /** @example Arabic */ + english_name?: string; + data?: { + /** @example */ + homepage?: string; + /** @example إدوارد يتعرض لضغوط حتى يصل به الحال إلى أنه لا يستطيع النوم لفتراتٍ طويلة، لكنه يجد بعض السلام في جلسات العلاج النفسي الجماعي، يتعرف إدوارد على أحد الأشخاص وهو (تايلر ديردن) الذي يحرره من تعلقه بالأشياء الذي تستعبده ،ثم يحرره من خوفه من الناس. يقومان معًا بإنشاء نادي القتال الذي يجذب الكثير من الأفراد المحبطين ،الذين يقومون بإخراج طاقة غضبهم وكرههم للعالم في القتال. */ + overview?: string; + /** + * @default 0 + * @example 0 + */ + runtime: number; + /** @example */ + tagline?: string; + /** @example */ + title?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'movie-videos': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + results?: { + /** @example en */ + iso_639_1?: string; + /** @example US */ + iso_3166_1?: string; + /** @example Fight Club (1999) Trailer - Starring Brad Pitt, Edward Norton, Helena Bonham Carter */ + name?: string; + /** @example O-b2VfmmbyA */ + key?: string; + /** @example YouTube */ + site?: string; + /** + * @default 0 + * @example 720 + */ + size: number; + /** @example Trailer */ + type?: string; + /** + * @default true + * @example false + */ + official: boolean; + /** @example 2016-03-05T02:03:14.000Z */ + published_at?: string; + /** @example 639d5326be6d88007f170f44 */ + id?: string; + }[]; + }; + }; + }; + }; + }; + 'movie-watch-providers': { + parameters: { + query?: never; + header?: never; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + results?: { + AE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AE */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + AL?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AL */ + link?: string; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + AR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AR */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + AT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AT */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + rent?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + AU?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AU */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + BA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BA */ + link?: string; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + BB?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BB */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + BE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BE */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + BG?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BG */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + BH?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BH */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + BO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BO */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + BR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BR */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + BS?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BS */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + CA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CA */ + link?: string; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /sB5vHrmYmliwUvBwZe8HpXo9r8m.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 305 + */ + provider_id: number; + /** @example Crave Starz */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + CH?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CH */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /rVOOhp6V8FheEAKtFAJMLMbnaMZ.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 150 + */ + provider_id: number; + /** @example blue TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + rent?: { + /** @example /rVOOhp6V8FheEAKtFAJMLMbnaMZ.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 150 + */ + provider_id: number; + /** @example blue TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + CL?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CL */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + CO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CO */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + CR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CR */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + CV?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CV */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + }; + CZ?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CZ */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /wTF37o4jOkQfjnWe41gmeuASYZA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 308 + */ + provider_id: number; + /** @example O2 TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + DE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DE */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + DK?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DK */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + DO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DO */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + EC?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EC */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + EE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EE */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + EG?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EG */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + ES?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ES */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + ads?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + FI?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FI */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + FJ?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FJ */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + FR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FR */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + GB?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GB */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + rent?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + GF?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GF */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + GI?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GI */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + GR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GR */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + GT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GT */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + HK?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HK */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + HN?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HN */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + HR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HR */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + ads?: { + /** @example /xrHrIraInfRXnrz1zHhY1tXJowg.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 572 + */ + provider_id: number; + /** @example RTL Play */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + HU?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HU */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + ID?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ID */ + link?: string; + flatrate?: { + /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 122 + */ + provider_id: number; + /** @example Hotstar */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + IE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IE */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + IL?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IL */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + IN?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IN */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + IQ?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IQ */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + IS?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IS */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + IT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IT */ + link?: string; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + rent?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + JM?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JM */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + JO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JO */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + JP?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JP */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + rent?: { + /** @example /g8jqHtXJsMlc8B1Gb0Rt8AvUJMn.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 85 + */ + provider_id: number; + /** @example dTV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + KR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=KR */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /2ioan5BX5L9tz4fIGU93blTeFhv.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 356 + */ + provider_id: number; + /** @example wavve */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + KW?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=KW */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + LB?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LB */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + LI?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LI */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + LT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LT */ + link?: string; + rent?: { + /** @example /xTVM8uXT9QocigQ07LE7Irc65W2.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 553 + */ + provider_id: number; + /** @example Telia Play */ + provider_name?: string; + /** + * @default 0 + * @example 15 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + LV?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LV */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + MD?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MD */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 26 + */ + display_priority: number; + }[]; + }; + MK?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MK */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + MT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MT */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + rent?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + MU?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MU */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 15 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 15 + */ + display_priority: number; + }[]; + }; + MX?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MX */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + MY?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MY */ + link?: string; + flatrate?: { + /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 122 + */ + provider_id: number; + /** @example Hotstar */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + MZ?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MZ */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 16 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 16 + */ + display_priority: number; + }[]; + }; + NL?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NL */ + link?: string; + buy?: { + /** @example /llmnYOyknekZsXtkCaazKjhTLvG.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 71 + */ + provider_id: number; + /** @example Pathé Thuis */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + rent?: { + /** @example /llmnYOyknekZsXtkCaazKjhTLvG.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 71 + */ + provider_id: number; + /** @example Pathé Thuis */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + NO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NO */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + NZ?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NZ */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + OM?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=OM */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PA */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PE */ + link?: string; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + PH?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PH */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PK?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PK */ + link?: string; + flatrate?: { + /** @example /t2yyOv40HZeVlLjYsCsPHnWLk4W.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 8 + */ + provider_id: number; + /** @example Netflix */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + PL?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PL */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PS?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PS */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PT */ + link?: string; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + rent?: { + /** @example /dUeHhim2WUZz8S7EWjv0Ws6anRP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 242 + */ + provider_id: number; + /** @example Meo */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + PY?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PY */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + QA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=QA */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + RO?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RO */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + RS?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RS */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + RU?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RU */ + link?: string; + rent?: { + /** @example /o9ExgOSLF3OTwR6T3DJOuwOKJgq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 113 + */ + provider_id: number; + /** @example Ivi */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + buy?: { + /** @example /o9ExgOSLF3OTwR6T3DJOuwOKJgq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 113 + */ + provider_id: number; + /** @example Ivi */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /zLM7f1w2L8TU2Fspzns72m6h3yY.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 501 + */ + provider_id: number; + /** @example Wink */ + provider_name?: string; + /** + * @default 0 + * @example 1000 + */ + display_priority: number; + }[]; + }; + SA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SA */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + SE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SE */ + link?: string; + buy?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + rent?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + SG?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SG */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + SI?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SI */ + link?: string; + buy?: { + /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 35 + */ + provider_id: number; + /** @example Rakuten TV */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + SK?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SK */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + SM?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SM */ + link?: string; + flatrate?: { + /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 337 + */ + provider_id: number; + /** @example Disney Plus */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + SV?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SV */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + TH?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TH */ + link?: string; + flatrate?: { + /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 122 + */ + provider_id: number; + /** @example Hotstar */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + TR?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TR */ + link?: string; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + TT?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TT */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + TW?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TW */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + UG?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=UG */ + link?: string; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 16 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 16 + */ + display_priority: number; + }[]; + }; + US?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=US */ + link?: string; + rent?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /jPXksae158ukMLFhhlNvzsvaEyt.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 257 + */ + provider_id: number; + /** @example fuboTV */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + UY?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=UY */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + VE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=VE */ + link?: string; + rent?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + YE?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=YE */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + }; + ZA?: { + /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ZA */ + link?: string; + flatrate?: { + /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 119 + */ + provider_id: number; + /** @example Amazon Prime Video */ + provider_name?: string; + /** + * @default 0 + * @example 1 + */ + display_priority: number; + }[]; + rent?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + }; + }; + }; + }; + }; + }; + 'movie-add-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header: { + 'Content-Type': string; + }; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + }; + }; + 'movie-delete-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header?: { + 'Content-Type'?: string; + }; + path: { + movie_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 13 + */ + status_code: number; + /** @example The item/record was deleted successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'network-details': { + parameters: { + query?: never; + header?: never; + path: { + network_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example New York City, New York */ + headquarters?: string; + /** @example https://www.hbo.com */ + homepage?: string; + /** + * @default 0 + * @example 49 + */ + id: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + logo_path?: string; + /** @example HBO */ + name?: string; + /** @example US */ + origin_country?: string; + }; + }; + }; + }; + }; + 'details-copy': { + parameters: { + query?: never; + header?: never; + path: { + network_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 49 + */ + id: number; + results?: { + /** @example Home Box Office */ + name?: string; + /** @example */ + type?: string; + }[]; + }; + }; + }; + }; + }; + 'alternative-names-copy': { + parameters: { + query?: never; + header?: never; + path: { + network_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 49 + */ + id: number; + logos?: { + /** + * @default 0 + * @example 2.425287356321839 + */ + aspect_ratio: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + file_path?: string; + /** + * @default 0 + * @example 174 + */ + height: number; + /** @example 5a7a67a40e0a26020a000091 */ + id?: string; + /** @example .svg */ + file_type?: string; + /** + * @default 0 + * @example 5.318 + */ + vote_average: number; + /** + * @default 0 + * @example 3 + */ + vote_count: number; + /** + * @default 0 + * @example 422 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'person-popular-list': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 1 + */ + gender: number; + /** + * @default 0 + * @example 224513 + */ + id: number; + known_for?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /ilRyazdMJwN05exqhwK4tMKBYZs.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 335984 + */ + id: number; + /** @example movie */ + media_type?: string; + /** @example en */ + original_language?: string; + /** @example Blade Runner 2049 */ + original_title?: string; + /** @example Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years. */ + overview?: string; + /** @example /gajva2L0rPYkEWjzgFlBXCAVBE5.jpg */ + poster_path?: string; + /** @example 2017-10-04 */ + release_date?: string; + /** @example Blade Runner 2049 */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.5 + */ + vote_average: number; + /** + * @default 0 + * @example 11771 + */ + vote_count: number; + }[]; + /** @example Acting */ + known_for_department?: string; + /** @example Ana de Armas */ + name?: string; + /** + * @default 0 + * @example 343.33 + */ + popularity: number; + /** @example /3vxvsmYLTf4jnr163SUlBIw51ee.jpg */ + profile_path?: string; + }[]; + /** + * @default 0 + * @example 500 + */ + total_pages: number; + /** + * @default 0 + * @example 10000 + */ + total_results: number; + }; + }; + }; + }; + }; + 'person-details': { + parameters: { + query?: { + /** @description comma separated list of endpoints within this namespace, 20 items max */ + append_to_response?: string; + language?: string; + }; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + also_known_as?: string[]; + /** + * @example Thomas Jeffrey Hanks (born July 9, 1956) is an American actor and filmmaker. Known for both his comedic and dramatic roles, Hanks is one of the most popular and recognizable film stars worldwide, and is widely regarded as an American cultural icon. + * + * Hanks made his breakthrough with leading roles in the comedies Splash (1984) and Big (1988). He won two consecutive Academy Awards for Best Actor for starring as a gay lawyer suffering from AIDS in Philadelphia (1993) and a young man with below-average IQ in Forrest Gump (1994). Hanks collaborated with film director Steven Spielberg on five films: Saving Private Ryan (1998), Catch Me If You Can (2002), The Terminal (2004), Bridge of Spies (2015), and The Post (2017), as well as the 2001 miniseries Band of Brothers, which launched him as a director, producer, and screenwriter. + * + * Hanks' other notable films include the romantic comedies Sleepless in Seattle (1993) and You've Got Mail (1998); the dramas Apollo 13 (1995), The Green Mile (1999), Cast Away (2000), Road to Perdition (2002), and Cloud Atlas (2012); and the biographical dramas Saving Mr. Banks (2013), Captain Phillips (2013), Sully (2016), and A Beautiful Day in the Neighborhood (2019). He has also appeared as the title character in the Robert Langdon film series, and has voiced Sheriff Woody in the Toy Story film series. + * + * Description above from the Wikipedia article Tom Hanks, licensed under CC-BY-SA, full list of contributors on Wikipedia. + */ + biography?: string; + /** @example 1956-07-09 */ + birthday?: string; + deathday?: unknown; + /** + * @default 0 + * @example 2 + */ + gender: number; + homepage?: unknown; + /** + * @default 0 + * @example 31 + */ + id: number; + /** @example nm0000158 */ + imdb_id?: string; + /** @example Acting */ + known_for_department?: string; + /** @example Tom Hanks */ + name?: string; + /** @example Concord, California, USA */ + place_of_birth?: string; + /** + * @default 0 + * @example 82.989 + */ + popularity: number; + /** @example /xndWFsBlClOJFRdhSt4NBwiPq2o.jpg */ + profile_path?: string; + }; + }; + }; + }; + }; + 'person-changes': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + changes?: { + /** @example biography */ + key?: string; + items?: { + /** @example 640469b113654500ba4e859a */ + id?: string; + /** @example added */ + action?: string; + /** @example 2023-03-05 10:06:41 UTC */ + time?: string; + /** @example ca */ + iso_639_1?: string; + /** @example ES */ + iso_3166_1?: string; + /** + * @example Thomas "Tom" Jeffrey Hanks (Concord, Califòrnia, 9 de juliol de 1956) és un actor de cinema i productor estatunidenc, guanyador dues vegades de l'Oscar al millor actor i considerat un dels més versàtils i talentosos del cinema actual. + * + * Hanks és l'actor que més diners ha guanyat de tota la història del cinema amb un total de gairebé sis mil milions de dòlars (setembre 2006). És també copropietari de Playtone, una companyia de producció de pel·lícules. + */ + value?: string; + }[]; + }[]; + }; + }; + }; + }; + }; + 'person-combined-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 13 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Forrest Gump */ + original_title?: string; + /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ + overview?: string; + /** + * @default 0 + * @example 62.225 + */ + popularity: number; + /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ + poster_path?: string; + /** @example 1994-06-23 */ + release_date?: string; + /** @example Forrest Gump */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.481 + */ + vote_average: number; + /** + * @default 0 + * @example 24535 + */ + vote_count: number; + /** @example Forrest Gump */ + character?: string; + /** @example 52fe420ec3a36847f800074f */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + /** @example movie */ + media_type?: string; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /tx3uj8GPWf5pzb0gWATJ4bokNHI.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 87061 + */ + id: number; + /** @example fr */ + original_language?: string; + /** @example Le Voyage extraordinaire */ + original_title?: string; + /** @example An account of the extraordinary life of film pioneer Georges Méliès (1861-1938) and the amazing story of the copy in color of his masterpiece “A Trip to the Moon” (1902), unexpectedly found in Spain and restored thanks to the heroic efforts of a group of true cinema lovers. */ + overview?: string; + /** + * @default 0 + * @example 6.007 + */ + popularity: number; + /** @example /zHNNT9gfiGsuadR6x38KYOp6ekq.jpg */ + poster_path?: string; + /** @example 2011-12-08 */ + release_date?: string; + /** @example The Extraordinary Voyage */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.6 + */ + vote_average: number; + /** + * @default 0 + * @example 47 + */ + vote_count: number; + /** @example 5d818a63d34eb3002c4f8fea */ + credit_id?: string; + /** @example Crew */ + department?: string; + /** @example Thanks */ + job?: string; + /** @example movie */ + media_type?: string; + }[]; + /** + * @default 0 + * @example 31 + */ + id: number; + }; + }; + }; + }; + }; + 'person-external-ids': { + parameters: { + query?: never; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 31 + */ + id: number; + /** @example /m/0bxtg */ + freebase_mid?: string; + /** @example /en/tom_hanks */ + freebase_id?: string; + /** @example nm0000158 */ + imdb_id?: string; + /** + * @default 0 + * @example 14293 + */ + tvrage_id: number; + /** @example Q2263 */ + wikidata_id?: string; + /** @example TomHanks */ + facebook_id?: string; + /** @example tomhanks */ + instagram_id?: string; + /** @example tomhanks */ + tiktok_id?: string; + /** @example tomhanks */ + twitter_id?: string; + youtube_id?: unknown; + }; + }; + }; + }; + }; + 'person-images': { + parameters: { + query?: never; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 287 + */ + id: number; + profiles?: { + /** + * @default 0 + * @example 0.666 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 980 + */ + height: number; + iso_639_1?: unknown; + /** @example /cckcYc2v0yh1tc9QjRelptcOBko.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.288 + */ + vote_average: number; + /** + * @default 0 + * @example 89 + */ + vote_count: number; + /** + * @default 0 + * @example 653 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'person-latest-id': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + also_known_as?: unknown[]; + /** @example */ + biography?: string; + birthday?: unknown; + deathday?: unknown; + /** + * @default 0 + * @example 0 + */ + gender: number; + homepage?: unknown; + /** + * @default 0 + * @example 4064343 + */ + id: number; + imdb_id?: unknown; + known_for_department?: unknown; + /** @example Ángel Cruz */ + name?: string; + place_of_birth?: unknown; + /** + * @default 0 + * @example 0 + */ + popularity: number; + profile_path?: unknown; + }; + }; + }; + }; + }; + 'person-movie-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 13 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Forrest Gump */ + original_title?: string; + /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ + overview?: string; + /** + * @default 0 + * @example 62.225 + */ + popularity: number; + /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ + poster_path?: string; + /** @example 1994-06-23 */ + release_date?: string; + /** @example Forrest Gump */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.481 + */ + vote_average: number; + /** + * @default 0 + * @example 24535 + */ + vote_count: number; + /** @example Forrest Gump */ + character?: string; + /** @example 52fe420ec3a36847f800074f */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /tx3uj8GPWf5pzb0gWATJ4bokNHI.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 87061 + */ + id: number; + /** @example fr */ + original_language?: string; + /** @example Le Voyage extraordinaire */ + original_title?: string; + /** @example An account of the extraordinary life of film pioneer Georges Méliès (1861-1938) and the amazing story of the copy in color of his masterpiece “A Trip to the Moon” (1902), unexpectedly found in Spain and restored thanks to the heroic efforts of a group of true cinema lovers. */ + overview?: string; + /** + * @default 0 + * @example 6.007 + */ + popularity: number; + /** @example /zHNNT9gfiGsuadR6x38KYOp6ekq.jpg */ + poster_path?: string; + /** @example 2011-12-08 */ + release_date?: string; + /** @example The Extraordinary Voyage */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.6 + */ + vote_average: number; + /** + * @default 0 + * @example 47 + */ + vote_count: number; + /** @example 5d818a63d34eb3002c4f8fea */ + credit_id?: string; + /** @example Crew */ + department?: string; + /** @example Thanks */ + job?: string; + }[]; + /** + * @default 0 + * @example 31 + */ + id: number; + }; + }; + }; + }; + }; + 'person-tv-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /ttvojTMgaIN7U8gqB5LlNqO4vPN.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1900 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example LIVE with Kelly and Mark */ + original_name?: string; + /** @example A morning talk show with A-list celebrity guests, top-notch performances and one-of-a-kind segments that are unrivaled on daytime television, plus spontaneous, hilarious and unpredictable talk. */ + overview?: string; + /** + * @default 0 + * @example 700.508 + */ + popularity: number; + /** @example /l5y8egG27p2fSTyq8s21SQMmQLy.jpg */ + poster_path?: string; + /** @example 1988-09-05 */ + first_air_date?: string; + /** @example LIVE with Kelly and Mark */ + name?: string; + /** + * @default 0 + * @example 5.4 + */ + vote_average: number; + /** + * @default 0 + * @example 25 + */ + vote_count: number; + /** @example */ + character?: string; + /** @example 52571af019c29571140d5c92 */ + credit_id?: string; + /** + * @default 0 + * @example 1 + */ + episode_count: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /6uMA6EAiwcsCqQJwWgYwtORvE0v.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 2391 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Tales from the Crypt */ + original_name?: string; + /** @example Cadaverous scream legend the Crypt Keeper is your macabre host for these forays of fright and fun based on the classic E.C. Comics tales from back in the day. So shamble up to the bar and pick your poison. Will it be an insane Santa on a personal slay ride? Honeymooners out to fulfill the "til death do we part" vow ASAP? */ + overview?: string; + /** + * @default 0 + * @example 24.88 + */ + popularity: number; + /** @example /dDfXQH6Kg2JNASI0dqNALukjhk1.jpg */ + poster_path?: string; + /** @example 1989-06-10 */ + first_air_date?: string; + /** @example Tales from the Crypt */ + name?: string; + /** + * @default 0 + * @example 7.978 + */ + vote_average: number; + /** + * @default 0 + * @example 757 + */ + vote_count: number; + /** @example 525734f3760ee3776a397211 */ + credit_id?: string; + /** @example Directing */ + department?: string; + /** + * @default 0 + * @example 1 + */ + episode_count: number; + /** @example Director */ + job?: string; + }[]; + /** + * @default 0 + * @example 31 + */ + id: number; + }; + }; + }; + }; + }; + 'person-tagged-images': { + parameters: { + query?: { + page?: number; + }; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 31 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default 0 + * @example 0.6666666666666666 + */ + aspect_ratio: number; + /** @example /1wY4psJ5NVEhCuOYROwLH2XExM2.jpg */ + file_path?: string; + /** + * @default 0 + * @example 1500 + */ + height: number; + /** @example 5b235d740e0a265b5d0031d9 */ + id?: string; + /** @example en */ + iso_639_1?: string; + /** + * @default 0 + * @example 5.456 + */ + vote_average: number; + /** + * @default 0 + * @example 7 + */ + vote_count: number; + /** + * @default 0 + * @example 1000 + */ + width: number; + /** @example poster */ + image_type?: string; + media?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /bdD39MpSVhKjxarTxLSfX6baoMP.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 857 + */ + id: number; + /** @example Saving Private Ryan */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Saving Private Ryan */ + original_title?: string; + /** @example As U.S. troops storm the beaches of Normandy, three brothers lie dead on the battlefield, with a fourth trapped behind enemy lines. Ranger captain John Miller and seven men are tasked with penetrating German-held territory and bringing the boy home. */ + overview?: string; + /** @example /uqx37cS8cpHg8U35f9U5IBlrCV3.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 70.45 + */ + popularity: number; + /** @example 1998-07-24 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.208 + */ + vote_average: number; + /** + * @default 0 + * @example 14134 + */ + vote_count: number; + }; + /** @example movie */ + media_type?: string; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 13 + */ + total_results: number; + }; + }; + }; + }; + }; + translations: { + parameters: { + query?: never; + header?: never; + path: { + person_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 31 + */ + id: number; + translations?: { + /** @example US */ + iso_3166_1?: string; + /** @example en */ + iso_639_1?: string; + /** @example English */ + name?: string; + /** @example English */ + english_name?: string; + data?: { + /** + * @example Thomas Jeffrey Hanks (born July 9, 1956) is an American actor and filmmaker. Known for both his comedic and dramatic roles, Hanks is one of the most popular and recognizable film stars worldwide, and is widely regarded as an American cultural icon. + * + * Hanks made his breakthrough with leading roles in the comedies Splash (1984) and Big (1988). He won two consecutive Academy Awards for Best Actor for starring as a gay lawyer suffering from AIDS in Philadelphia (1993) and a young man with below-average IQ in Forrest Gump (1994). Hanks collaborated with film director Steven Spielberg on five films: Saving Private Ryan (1998), Catch Me If You Can (2002), The Terminal (2004), Bridge of Spies (2015), and The Post (2017), as well as the 2001 miniseries Band of Brothers, which launched him as a director, producer, and screenwriter. + * + * Hanks' other notable films include the romantic comedies Sleepless in Seattle (1993) and You've Got Mail (1998); the dramas Apollo 13 (1995), The Green Mile (1999), Cast Away (2000), Road to Perdition (2002), and Cloud Atlas (2012); and the biographical dramas Saving Mr. Banks (2013), Captain Phillips (2013), Sully (2016), and A Beautiful Day in the Neighborhood (2019). He has also appeared as the title character in the Robert Langdon film series, and has voiced Sheriff Woody in the Toy Story film series. + * + * Description above from the Wikipedia article Tom Hanks, licensed under CC-BY-SA, full list of contributors on Wikipedia. + */ + biography?: string; + /** @example Tom Hanks */ + name?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'review-details': { + parameters: { + query?: never; + header?: never; + path: { + review_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example 640b2aeecaaca20079decdcc */ + id?: string; + /** @example Ricardo Oliveira */ + author?: string; + author_details?: { + /** @example Ricardo Oliveira */ + name?: string; + /** @example RSOliveira */ + username?: string; + /** @example /23Cl7rhsknc7IIAcZZAGKzovjTu.jpg */ + avatar_path?: string; + /** + * @default 0 + * @example 9 + */ + rating: number; + }; + /** + * @example "The Last of Us" is a post-apocalyptic TV series based on the popular video game of the same name. The story follows the journey of Joel, a smuggler, and Ellie, a teenage girl who may be the key to finding a cure for a deadly fungal infection that has ravaged the world. + * + * The series features outstanding performances from Pedro Pascal as Joel, Bella Ramsey as Ellie, and Anna Torv as Tess. The chemistry between the main characters is excellent, and the casting is spot-on. + * + * The show's writing is superb, and it captures the essence of the video game while adding a fresh perspective. The narrative is engaging, and the pacing is just right, with each episode leaving you on the edge of your seat, eager to see what happens next. + * + * The show's production value is top-notch, with stunning visuals and cinematography that capture the bleak and haunting atmosphere of a post-apocalyptic world. The use of practical effects and makeup is impressive and adds to the overall immersion of the story. + * + * Overall, "The Last of Us" is an outstanding TV series that does justice to the source material. It's a must-watch for fans of the video game and anyone who enjoys gripping and emotional storytelling. I would rate it a 9 out of 10. + * + * + * + * Written and Reviewed by RSOliveira + */ + content?: string; + /** @example 2023-03-10T13:04:46.674Z */ + created_at?: string; + /** @example en */ + iso_639_1?: string; + /** + * @default 0 + * @example 100088 + */ + media_id: number; + /** @example The Last of Us */ + media_title?: string; + /** @example tv */ + media_type?: string; + /** @example 2023-03-10T13:04:46.734Z */ + updated_at?: string; + /** @example https://www.themoviedb.org/review/640b2aeecaaca20079decdcc */ + url?: string; + }; + }; + }; + }; + }; + 'search-collection': { + parameters: { + query: { + query: string; + include_adult?: boolean; + language?: string; + page?: number; + region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /zuW6fOiusv4X9nnW3paHGfXcSll.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 86311 + */ + id: number; + /** @example The Avengers Collection */ + name?: string; + /** @example en */ + original_language?: string; + /** @example The Avengers Collection */ + original_name?: string; + /** @example A superhero film series produced by Marvel Studios based on the Marvel Comics superhero team of the same name, and part of the Marvel Cinematic Universe (MCU). The series features an ensemble cast from the Marvel Cinematic Universe series films, as they join forces for the peacekeeping organization S.H.I.E.L.D. led by Nick Fury. */ + overview?: string; + /** @example /yFSIUVTCvgYrpalUktulvk3Gi5Y.jpg */ + poster_path?: string; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-company': { + parameters: { + query: { + query: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default 0 + * @example 3268 + */ + id: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + logo_path?: string; + /** @example HBO */ + name?: string; + /** @example US */ + origin_country?: string; + }[]; + /** + * @default 0 + * @example 2 + */ + total_pages: number; + /** + * @default 0 + * @example 22 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-keyword': { + parameters: { + query: { + query: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default 0 + * @example 262419 + */ + id: number; + /** @example lost */ + name?: string; + }[]; + /** + * @default 0 + * @example 5 + */ + total_pages: number; + /** + * @default 0 + * @example 84 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-movie': { + parameters: { + query: { + query: string; + include_adult?: boolean; + language?: string; + primary_release_year?: string; + page?: number; + region?: string; + year?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 550 + */ + id: number; + /** @example en */ + original_language?: string; + /** @example Fight Club */ + original_title?: string; + /** @example A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion. */ + overview?: string; + /** + * @default 0 + * @example 73.433 + */ + popularity: number; + /** @example /pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg */ + poster_path?: string; + /** @example 1999-10-15 */ + release_date?: string; + /** @example Fight Club */ + title?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.433 + */ + vote_average: number; + /** + * @default 0 + * @example 26279 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 2 + */ + total_pages: number; + /** + * @default 0 + * @example 39 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-multi': { + parameters: { + query: { + query: string; + include_adult?: boolean; + language?: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /aDYSnJAK0BTVeE8osOy22Kz3SXY.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 11 + */ + id: number; + /** @example Star Wars */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Star Wars */ + original_title?: string; + /** @example Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire. */ + overview?: string; + /** @example /6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 78.047 + */ + popularity: number; + /** @example 1977-05-25 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.208 + */ + vote_average: number; + /** + * @default 0 + * @example 18528 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 11 + */ + total_pages: number; + /** + * @default 0 + * @example 201 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-person': { + parameters: { + query: { + query: string; + include_adult?: boolean; + language?: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 31 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Tom Hanks */ + name?: string; + /** @example Tom Hanks */ + original_name?: string; + /** + * @default 0 + * @example 84.631 + */ + popularity: number; + /** @example /xndWFsBlClOJFRdhSt4NBwiPq2o.jpg */ + profile_path?: string; + known_for?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 13 + */ + id: number; + /** @example Forrest Gump */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Forrest Gump */ + original_title?: string; + /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ + overview?: string; + /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 67.209 + */ + popularity: number; + /** @example 1994-06-23 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 8.481 + */ + vote_average: number; + /** + * @default 0 + * @example 24525 + */ + vote_count: number; + }[]; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'search-tv': { + parameters: { + query: { + query: string; + /** @description Search only the first air date. Valid values are: 1000..9999 */ + first_air_date_year?: number; + include_adult?: boolean; + language?: string; + page?: number; + /** @description Search the first air date and all episode air dates. Valid values are: 1000..9999 */ + year?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1396 + */ + id: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Breaking Bad */ + original_name?: string; + /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ + overview?: string; + /** + * @default 0 + * @example 298.884 + */ + popularity: number; + /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ + poster_path?: string; + /** @example 2008-01-20 */ + first_air_date?: string; + /** @example Breaking Bad */ + name?: string; + /** + * @default 0 + * @example 8.879 + */ + vote_average: number; + /** + * @default 0 + * @example 11536 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 1 + */ + total_results: number; + }; + }; + }; + }; + }; + 'trending-all': { + parameters: { + query?: { + /** @description `ISO-639-1`-`ISO-3166-1` code */ + language?: string; + }; + header?: never; + path: { + time_window: 'day' | 'week'; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 934433 + */ + id: number; + /** @example Scream VI */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Scream VI */ + original_title?: string; + /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ + overview?: string; + /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 609.941 + */ + popularity: number; + /** @example 2023-03-08 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.374 + */ + vote_average: number; + /** + * @default 0 + * @example 684 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 1000 + */ + total_pages: number; + /** + * @default 0 + * @example 20000 + */ + total_results: number; + }; + }; + }; + }; + }; + 'trending-movies': { + parameters: { + query?: { + /** @description `ISO-639-1`-`ISO-3166-1` code */ + language?: string; + }; + header?: never; + path: { + time_window: 'day' | 'week'; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 934433 + */ + id: number; + /** @example Scream VI */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Scream VI */ + original_title?: string; + /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ + overview?: string; + /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 609.941 + */ + popularity: number; + /** @example 2023-03-08 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.374 + */ + vote_average: number; + /** + * @default 0 + * @example 684 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 1000 + */ + total_pages: number; + /** + * @default 0 + * @example 20000 + */ + total_results: number; + }; + }; + }; + }; + }; + 'trending-people': { + parameters: { + query?: { + /** @description `ISO-639-1`-`ISO-3166-1` code */ + language?: string; + }; + header?: never; + path: { + time_window: 'day' | 'week'; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 224513 + */ + id: number; + /** @example Ana de Armas */ + name?: string; + /** @example Ana de Armas */ + original_name?: string; + /** @example person */ + media_type?: string; + /** + * @default 0 + * @example 349.766 + */ + popularity: number; + /** + * @default 0 + * @example 1 + */ + gender: number; + /** @example Acting */ + known_for_department?: string; + /** @example /3vxvsmYLTf4jnr163SUlBIw51ee.jpg */ + profile_path?: string; + known_for?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /ilRyazdMJwN05exqhwK4tMKBYZs.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 335984 + */ + id: number; + /** @example Blade Runner 2049 */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Blade Runner 2049 */ + original_title?: string; + /** @example Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years. */ + overview?: string; + /** @example /gajva2L0rPYkEWjzgFlBXCAVBE5.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 79.571 + */ + popularity: number; + /** @example 2017-10-04 */ + release_date?: string; + /** + * @default true + * @example false + */ + video: boolean; + /** + * @default 0 + * @example 7.531 + */ + vote_average: number; + /** + * @default 0 + * @example 11771 + */ + vote_count: number; + }[]; + }[]; + /** + * @default 0 + * @example 1000 + */ + total_pages: number; + /** + * @default 0 + * @example 20000 + */ + total_results: number; + }; + }; + }; + }; + }; + 'trending-tv': { + parameters: { + query?: { + /** @description `ISO-639-1`-`ISO-3166-1` code */ + language?: string; + }; + header?: never; + path: { + time_window: 'day' | 'week'; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /8P15FsYcTwQZ4G5rRMd1TKD14Aq.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 103768 + */ + id: number; + /** @example Sweet Tooth */ + name?: string; + /** @example en */ + original_language?: string; + /** @example Sweet Tooth */ + original_name?: string; + /** @example On a perilous adventure across a post-apocalyptic world, a lovable boy who's half-human and half-deer searches for a new beginning with a gruff protector. */ + overview?: string; + /** @example /dBxxtfhC4vYrxB2fLsSxOTY2dQc.jpg */ + poster_path?: string; + /** @example tv */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 137.498 + */ + popularity: number; + /** @example 2021-06-04 */ + first_air_date?: string; + /** + * @default 0 + * @example 7.928 + */ + vote_average: number; + /** + * @default 0 + * @example 1094 + */ + vote_count: number; + origin_country?: string[]; + }[]; + /** + * @default 0 + * @example 1000 + */ + total_pages: number; + /** + * @default 0 + * @example 20000 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-airing-today-list': { + parameters: { + query?: { + language?: string; + page?: number; + timezone?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ + backdrop_path?: string; + /** @example 2023-01-23 */ + first_air_date?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 202250 + */ + id: number; + /** @example Dirty Linen */ + name?: string; + origin_country?: string[]; + /** @example tl */ + original_language?: string; + /** @example Dirty Linen */ + original_name?: string; + /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ + overview?: string; + /** + * @default 0 + * @example 2797.914 + */ + popularity: number; + /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 5 + */ + vote_average: number; + /** + * @default 0 + * @example 13 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 14 + */ + total_pages: number; + /** + * @default 0 + * @example 265 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-on-the-air-list': { + parameters: { + query?: { + language?: string; + page?: number; + timezone?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ + backdrop_path?: string; + /** @example 2023-01-23 */ + first_air_date?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 202250 + */ + id: number; + /** @example Dirty Linen */ + name?: string; + origin_country?: string[]; + /** @example tl */ + original_language?: string; + /** @example Dirty Linen */ + original_name?: string; + /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ + overview?: string; + /** + * @default 0 + * @example 2797.914 + */ + popularity: number; + /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 5 + */ + vote_average: number; + /** + * @default 0 + * @example 13 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 58 + */ + total_pages: number; + /** + * @default 0 + * @example 1151 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-popular-list': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ + backdrop_path?: string; + /** @example 2023-01-23 */ + first_air_date?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 202250 + */ + id: number; + /** @example Dirty Linen */ + name?: string; + origin_country?: string[]; + /** @example tl */ + original_language?: string; + /** @example Dirty Linen */ + original_name?: string; + /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ + overview?: string; + /** + * @default 0 + * @example 2797.914 + */ + popularity: number; + /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 5 + */ + vote_average: number; + /** + * @default 0 + * @example 13 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 7416 + */ + total_pages: number; + /** + * @default 0 + * @example 148302 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-top-rated-list': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example /99vBORZixICa32Pwdwj0lWcr8K.jpg */ + backdrop_path?: string; + /** @example 2021-09-03 */ + first_air_date?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 130392 + */ + id: number; + /** @example The D'Amelio Show */ + name?: string; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example The D'Amelio Show */ + original_name?: string; + /** @example From relative obscurity and a seemingly normal life, to overnight success and thrust into the Hollywood limelight overnight, the D’Amelios are faced with new challenges and opportunities they could not have imagined. */ + overview?: string; + /** + * @default 0 + * @example 12.459 + */ + popularity: number; + /** @example /phv2Jc4H8cvRzvTKb9X1uKMboTu.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 8.9 + */ + vote_average: number; + /** + * @default 0 + * @example 3190 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 142 + */ + total_pages: number; + /** + * @default 0 + * @example 2833 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-details': { + parameters: { + query?: { + /** @description comma separated list of endpoints within this namespace, 20 items max */ + append_to_response?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /6LWy0jvMpmjoS9fojNgHIKoWL05.jpg */ + backdrop_path?: string; + created_by?: { + /** + * @default 0 + * @example 9813 + */ + id: number; + /** @example 5256c8c219c2956ff604858a */ + credit_id?: string; + /** @example David Benioff */ + name?: string; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** @example /xvNN5huL0X8yJ7h3IZfGG4O2zBD.jpg */ + profile_path?: string; + }[]; + episode_run_time?: number[]; + /** @example 2011-04-17 */ + first_air_date?: string; + genres?: { + /** + * @default 0 + * @example 10765 + */ + id: number; + /** @example Sci-Fi & Fantasy */ + name?: string; + }[]; + /** @example http://www.hbo.com/game-of-thrones */ + homepage?: string; + /** + * @default 0 + * @example 1399 + */ + id: number; + /** + * @default true + * @example false + */ + in_production: boolean; + languages?: string[]; + /** @example 2019-05-19 */ + last_air_date?: string; + last_episode_to_air?: { + /** + * @default 0 + * @example 1551830 + */ + id: number; + /** @example The Iron Throne */ + name?: string; + /** @example In the aftermath of the devastating attack on King's Landing, Daenerys must face the survivors. */ + overview?: string; + /** + * @default 0 + * @example 4.809 + */ + vote_average: number; + /** + * @default 0 + * @example 241 + */ + vote_count: number; + /** @example 2019-05-19 */ + air_date?: string; + /** + * @default 0 + * @example 6 + */ + episode_number: number; + /** @example 806 */ + production_code?: string; + /** + * @default 0 + * @example 80 + */ + runtime: number; + /** + * @default 0 + * @example 8 + */ + season_number: number; + /** + * @default 0 + * @example 1399 + */ + show_id: number; + /** @example /zBi2O5EJfgTS6Ae0HdAYLm9o2nf.jpg */ + still_path?: string; + }; + /** @example Game of Thrones */ + name?: string; + next_episode_to_air?: unknown; + networks?: { + /** + * @default 0 + * @example 49 + */ + id: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + logo_path?: string; + /** @example HBO */ + name?: string; + /** @example US */ + origin_country?: string; + }[]; + /** + * @default 0 + * @example 73 + */ + number_of_episodes: number; + /** + * @default 0 + * @example 8 + */ + number_of_seasons: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Game of Thrones */ + original_name?: string; + /** @example Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond. */ + overview?: string; + /** + * @default 0 + * @example 346.098 + */ + popularity: number; + /** @example /1XS1oqL89opfnbLl8WnZY1O1uJx.jpg */ + poster_path?: string; + production_companies?: { + /** + * @default 0 + * @example 76043 + */ + id: number; + /** @example /9RO2vbQ67otPrBLXCaC8UMp3Qat.png */ + logo_path?: string; + /** @example Revolution Sun Studios */ + name?: string; + /** @example US */ + origin_country?: string; + }[]; + production_countries?: { + /** @example GB */ + iso_3166_1?: string; + /** @example United Kingdom */ + name?: string; + }[]; + seasons?: { + /** @example 2010-12-05 */ + air_date?: string; + /** + * @default 0 + * @example 272 + */ + episode_count: number; + /** + * @default 0 + * @example 3627 + */ + id: number; + /** @example Specials */ + name?: string; + /** @example */ + overview?: string; + /** @example /kMTcwNRfFKCZ0O2OaBZS0nZ2AIe.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 0 + */ + season_number: number; + /** + * @default 0 + * @example 0 + */ + vote_average: number; + }[]; + spoken_languages?: { + /** @example English */ + english_name?: string; + /** @example en */ + iso_639_1?: string; + /** @example English */ + name?: string; + }[]; + /** @example Ended */ + status?: string; + /** @example Winter Is Coming */ + tagline?: string; + /** @example Scripted */ + type?: string; + /** + * @default 0 + * @example 8.438 + */ + vote_average: number; + /** + * @default 0 + * @example 21390 + */ + vote_count: number; + }; + }; + }; + }; + }; + 'tv-series-account-states': { + parameters: { + query?: { + session_id?: string; + guest_session_id?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** + * @default true + * @example true + */ + favorite: boolean; + rated?: { + /** + * @default 0 + * @example 9 + */ + value: number; + }; + /** + * @default true + * @example false + */ + watchlist: boolean; + }; + }; + }; + }; + }; + 'tv-series-aggregate-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 1 + */ + gender: number; + /** + * @default 0 + * @example 1223786 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Emilia Clarke */ + name?: string; + /** @example Emilia Clarke */ + original_name?: string; + /** + * @default 0 + * @example 42.737 + */ + popularity: number; + /** @example /u59kTmNHXzaGZqokivxLPiBVIML.jpg */ + profile_path?: string; + roles?: { + /** @example 5256c8af19c2956ff60479f6 */ + credit_id?: string; + /** @example Daenerys Targaryen */ + character?: string; + /** + * @default 0 + * @example 78 + */ + episode_count: number; + }[]; + /** + * @default 0 + * @example 78 + */ + total_episode_count: number; + /** + * @default 0 + * @example 6 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 1 + */ + gender: number; + /** + * @default 0 + * @example 6411 + */ + id: number; + /** @example Art */ + known_for_department?: string; + /** @example Deborah Riley */ + name?: string; + /** @example Deborah Riley */ + original_name?: string; + /** + * @default 0 + * @example 1.4 + */ + popularity: number; + /** @example /cjhADpqdrnwB1PdDUKaBnWrIj2Q.jpg */ + profile_path?: string; + jobs?: { + /** @example 54eee9e5c3a3686d5800584e */ + credit_id?: string; + /** @example Production Design */ + job?: string; + /** + * @default 0 + * @example 43 + */ + episode_count: number; + }[]; + /** @example Art */ + department?: string; + /** + * @default 0 + * @example 43 + */ + total_episode_count: number; + }[]; + /** + * @default 0 + * @example 1399 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-series-alternative-titles': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + results?: { + /** @example AL */ + iso_3166_1?: string; + /** @example Froni i shpatave */ + title?: string; + /** @example */ + type?: string; + }[]; + }; + }; + }; + }; + }; + 'tv-series-changes': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + changes?: { + /** @example images */ + key?: string; + items?: { + /** @example 640435cf021cee0084710972 */ + id?: string; + /** @example updated */ + action?: string; + /** @example 2023-03-05 06:25:19 UTC */ + time?: string; + /** @example en */ + iso_639_1?: string; + /** @example */ + iso_3166_1?: string; + value?: { + poster?: { + /** @example /ouudK6RCNnsbT1CSXrlATXQIQTG.jpg */ + file_path?: string; + /** @example en */ + iso_639_1?: string; + }; + }; + original_value?: { + poster?: { + /** @example /ouudK6RCNnsbT1CSXrlATXQIQTG.jpg */ + file_path?: string; + /** @example fr */ + iso_639_1?: string; + }; + }; + }[]; + }[]; + }; + }; + }; + }; + }; + 'tv-series-content-ratings': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + descriptors?: unknown[]; + /** @example DE */ + iso_3166_1?: string; + /** @example 16 */ + rating?: string; + }[]; + /** + * @default 0 + * @example 1399 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-series-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 22970 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Peter Dinklage */ + name?: string; + /** @example Peter Dinklage */ + original_name?: string; + /** + * @default 0 + * @example 30.6 + */ + popularity: number; + /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ + profile_path?: string; + /** @example Tyrion Lannister */ + character?: string; + /** @example 5256c8b219c2956ff6047cd8 */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 1406855 + */ + id: number; + /** @example Production */ + known_for_department?: string; + /** @example Duncan Muggoch */ + name?: string; + /** @example Duncan Muggoch */ + original_name?: string; + /** + * @default 0 + * @example 1.592 + */ + popularity: number; + /** @example /ukGjJ62Ejd4cFziald03G34Fsrp.jpg */ + profile_path?: string; + /** @example 5ceab029c3a3682e93217a85 */ + credit_id?: string; + /** @example Production */ + department?: string; + /** @example Producer */ + job?: string; + }[]; + /** + * @default 0 + * @example 1399 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-series-episode-groups': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + /** @example */ + description?: string; + /** + * @default 0 + * @example 102 + */ + episode_count: number; + /** + * @default 0 + * @example 9 + */ + group_count: number; + /** @example 5e9077d2e640d600151f32bd */ + id?: string; + /** @example Aired Order */ + name?: string; + network?: { + /** + * @default 0 + * @example 49 + */ + id: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + logo_path?: string; + /** @example HBO */ + name?: string; + /** @example US */ + origin_country?: string; + }; + /** + * @default 0 + * @example 1 + */ + type: number; + }[]; + /** + * @default 0 + * @example 1399 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-series-external-ids': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + /** @example tt0944947 */ + imdb_id?: string; + /** @example /m/0524b41 */ + freebase_mid?: string; + /** @example /en/game_of_thrones */ + freebase_id?: string; + /** + * @default 0 + * @example 121361 + */ + tvdb_id: number; + /** + * @default 0 + * @example 24493 + */ + tvrage_id: number; + /** @example Q23572 */ + wikidata_id?: string; + /** @example GameOfThrones */ + facebook_id?: string; + /** @example gameofthrones */ + instagram_id?: string; + /** @example GameOfThrones */ + twitter_id?: string; + }; + }; + }; + }; + }; + 'tv-series-images': { + parameters: { + query?: { + /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ + include_image_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + backdrops?: { + /** + * @default 0 + * @example 1.778 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 800 + */ + height: number; + iso_639_1?: unknown; + /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.622 + */ + vote_average: number; + /** + * @default 0 + * @example 20 + */ + vote_count: number; + /** + * @default 0 + * @example 1422 + */ + width: number; + }[]; + /** + * @default 0 + * @example 550 + */ + id: number; + logos?: { + /** + * @default 0 + * @example 5.203 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 79 + */ + height: number; + /** @example he */ + iso_639_1?: string; + /** @example /c1KLulrIhUqY5fT42nmC5aERGCp.png */ + file_path?: string; + /** + * @default 0 + * @example 5.312 + */ + vote_average: number; + /** + * @default 0 + * @example 1 + */ + vote_count: number; + /** + * @default 0 + * @example 411 + */ + width: number; + }[]; + posters?: { + /** + * @default 0 + * @example 0.667 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 900 + */ + height: number; + /** @example pt */ + iso_639_1?: string; + /** @example /r3pPehX4ik8NLYPpbDRAh0YRtMb.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.258 + */ + vote_average: number; + /** + * @default 0 + * @example 6 + */ + vote_count: number; + /** + * @default 0 + * @example 600 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'tv-series-keywords': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + results?: { + /** @example based on novel or book */ + name?: string; + /** + * @default 0 + * @example 818 + */ + id: number; + }[]; + }; + }; + }; + }; + }; + 'tv-series-latest-id': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default true + * @example false + */ + adult: boolean; + backdrop_path?: unknown; + created_by?: unknown[]; + episode_run_time?: unknown[]; + /** @example */ + first_air_date?: string; + genres?: unknown[]; + /** @example */ + homepage?: string; + /** + * @default 0 + * @example 225491 + */ + id: number; + /** + * @default true + * @example true + */ + in_production: boolean; + languages?: unknown[]; + /** @example 2023-04-21 */ + last_air_date?: string; + last_episode_to_air?: { + /** + * @default 0 + * @example 4398801 + */ + id: number; + /** @example Episode 8 */ + name?: string; + /** @example */ + overview?: string; + /** + * @default 0 + * @example 0 + */ + vote_average: number; + /** + * @default 0 + * @example 0 + */ + vote_count: number; + /** @example 2023-04-21 */ + air_date?: string; + /** + * @default 0 + * @example 8 + */ + episode_number: number; + /** @example */ + production_code?: string; + runtime?: unknown; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** + * @default 0 + * @example 225491 + */ + show_id: number; + still_path?: unknown; + }; + /** @example 妖怪传 */ + name?: string; + next_episode_to_air?: unknown; + networks?: unknown[]; + /** + * @default 0 + * @example 1 + */ + number_of_episodes: number; + /** + * @default 0 + * @example 1 + */ + number_of_seasons: number; + origin_country?: string[]; + /** @example zh */ + original_language?: string; + /** @example 妖怪传 */ + original_name?: string; + /** @example */ + overview?: string; + /** + * @default 0 + * @example 0 + */ + popularity: number; + poster_path?: unknown; + production_companies?: unknown[]; + production_countries?: unknown[]; + seasons?: { + air_date?: unknown; + /** + * @default 0 + * @example 1 + */ + episode_count: number; + /** + * @default 0 + * @example 338956 + */ + id: number; + /** @example Season 1 */ + name?: string; + /** @example */ + overview?: string; + poster_path?: unknown; + /** + * @default 0 + * @example 1 + */ + season_number: number; + }[]; + spoken_languages?: unknown[]; + /** @example Returning Series */ + status?: string; + /** @example */ + tagline?: string; + /** @example Scripted */ + type?: string; + /** + * @default 0 + * @example 0 + */ + vote_average: number; + /** + * @default 0 + * @example 0 + */ + vote_count: number; + }; + }; + }; + }; + }; + 'lists-copy': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example */ + description?: string; + /** + * @default 0 + * @example 0 + */ + favorite_count: number; + /** + * @default 0 + * @example 8257231 + */ + id: number; + /** + * @default 0 + * @example 182 + */ + item_count: number; + /** @example en */ + iso_639_1?: string; + /** @example US */ + iso_3166_1?: string; + /** @example Done */ + name?: string; + poster_path?: unknown; + }[]; + /** + * @default 0 + * @example 96 + */ + total_pages: number; + /** + * @default 0 + * @example 1906 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-recommendations': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 1396 + */ + id: number; + /** @example Breaking Bad */ + name?: string; + /** @example en */ + original_language?: string; + /** @example Breaking Bad */ + original_name?: string; + /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ + overview?: string; + /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ + poster_path?: string; + /** @example tv */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 292.904 + */ + popularity: number; + /** @example 2008-01-20 */ + first_air_date?: string; + /** + * @default 0 + * @example 8.878 + */ + vote_average: number; + /** + * @default 0 + * @example 11544 + */ + vote_count: number; + origin_country?: string[]; + }[]; + /** + * @default 0 + * @example 2 + */ + total_pages: number; + /** + * @default 0 + * @example 40 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-reviews': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** @example lmao7 */ + author?: string; + author_details?: { + /** @example lmao7 */ + name?: string; + /** @example lmao7 */ + username?: string; + /** @example /ekmYOUU4tfx9zGGadjRdE7UPce.jpg */ + avatar_path?: string; + /** + * @default 0 + * @example 9 + */ + rating: number; + }; + /** + * @example I started watching when it came out as I heard that fans of LOTR also liked this. I stopped watching after Season 1 as I was devastated lol kinda. Only 2015 I decided to continue watching and got addicted like it seemed complicated at first, too many stories and characters. I even used a guide from internet like family tree per house while watching or GOT wiki so I can have more background on the characters. For a TV series, this show can really take you to a different world and never knowing what will happen. It is very daring that any time anybody can just die (I learned not to be attached and have accepted that they will all die so I won't be devastated hehe). I have never read the books but the show is entertaining and you will really root for your faves and really hate on those you hate. + * + * Fantasy, action, drama, comedy, love...and lots of surprises! + */ + content?: string; + /** @example 2017-02-20T05:47:28.872Z */ + created_at?: string; + /** @example 58aa82f09251416f92006a3a */ + id?: string; + /** @example 2021-06-23T15:57:54.649Z */ + updated_at?: string; + /** @example https://www.themoviedb.org/review/58aa82f09251416f92006a3a */ + url?: string; + }[]; + /** + * @default 0 + * @example 1 + */ + total_pages: number; + /** + * @default 0 + * @example 11 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-screened-theatrically': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + results?: { + /** + * @default 0 + * @example 1159054 + */ + id: number; + /** + * @default 0 + * @example 10 + */ + episode_number: number; + /** + * @default 0 + * @example 5 + */ + season_number: number; + }[]; + }; + }; + }; + }; + }; + 'tv-series-similar': { + parameters: { + query?: { + language?: string; + page?: number; + }; + header?: never; + path: { + series_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + page: number; + results?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** @example /zcFSvWa34nDn2NcqOPuthyOIBWT.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 197063 + */ + id: number; + origin_country?: string[]; + /** @example ko */ + original_language?: string; + /** @example 종이달 */ + original_name?: string; + /** @example A thriller drama about Yoo I-hwa, a stay-at-home mom living her comfortable and contented life without desires, but to her husband's indifference. While working as a bank contract employee, she unexpectedly touches money from VIP clients and gradually falls into an irreversible collapse. */ + overview?: string; + /** + * @default 0 + * @example 12.299 + */ + popularity: number; + /** @example /xXWynVdMGyJXBUDvIN27AXM3iJJ.jpg */ + poster_path?: string; + /** @example 2023-04-10 */ + first_air_date?: string; + /** @example Pale Moon */ + name?: string; + /** + * @default 0 + * @example 7 + */ + vote_average: number; + /** + * @default 0 + * @example 2 + */ + vote_count: number; + }[]; + /** + * @default 0 + * @example 82 + */ + total_pages: number; + /** + * @default 0 + * @example 1639 + */ + total_results: number; + }; + }; + }; + }; + }; + 'tv-series-translations': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + translations?: { + /** @example SA */ + iso_3166_1?: string; + /** @example ar */ + iso_639_1?: string; + /** @example العربية */ + name?: string; + /** @example Arabic */ + english_name?: string; + data?: { + /** @example صراع العروش */ + name?: string; + /** @example تتقاتل سبع عائلات نبيلة من أجل السيطرة على أرض - ويستيروس - الأسطورية. الاحتكاك بين العوائل يؤدي إلى حرب واسعة النطاق. في حين يستيقظ الشر القديم في أقصى الشمال. وفي خضم الحرب، نظام عسكري مهمَل - حرس الليل - هم كل ما يقف بين عالم الإنسان والأهوال الجليدية. */ + overview?: string; + /** @example */ + homepage?: string; + /** @example الشتاء قادم */ + tagline?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'tv-series-videos': { + parameters: { + query?: { + /** @description filter the list results by language, supports more than one value by using a comma */ + include_video_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + results?: { + /** @example en */ + iso_639_1?: string; + /** @example US */ + iso_3166_1?: string; + /** @example Inside Game of Thrones: A Story in Camera Work – BTS (HBO) */ + name?: string; + /** @example y2ZJ3lTaREY */ + key?: string; + /** @example YouTube */ + site?: string; + /** + * @default 0 + * @example 1080 + */ + size: number; + /** @example Behind the Scenes */ + type?: string; + /** + * @default true + * @example true + */ + official: boolean; + /** @example 2019-03-25T14:00:06.000Z */ + published_at?: string; + /** @example 5c999b48c3a36863b73b9d42 */ + id?: string; + }[]; + }; + }; + }; + }; + }; + 'tv-series-watch-providers': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1399 + */ + id: number; + results?: { + AE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AE */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + AR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AR */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + AT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AT */ + link?: string; + buy?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /y0kyIFElN5sJAsmW8Txj69wzrD2.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 321 + */ + provider_id: number; + /** @example Sky X */ + provider_name?: string; + /** + * @default 0 + * @example 23 + */ + display_priority: number; + }[]; + }; + AU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AU */ + link?: string; + flatrate?: { + /** @example /d3ixI1no0EpTj2i7u0Sd2DBXVlG.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 385 + */ + provider_id: number; + /** @example BINGE */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + BA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BA */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + BB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BB */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + BE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BE */ + link?: string; + flatrate?: { + /** @example /pq8p1umEnJjdFAP1nFvNArTR61X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 311 + */ + provider_id: number; + /** @example Be TV Go */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + BG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BG */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 15 + */ + display_priority: number; + }[]; + }; + BO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BO */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + BR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BR */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + BS?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BS */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + CA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CA */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /gJ3yVMWouaVj6iHd59TISJ1TlM5.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 230 + */ + provider_id: number; + /** @example Crave */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + CH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CH */ + link?: string; + flatrate?: { + /** @example /sHP8XLo4Ac4WMbziRyAdRQdb76q.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 210 + */ + provider_id: number; + /** @example Sky */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + buy?: { + /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + CI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CI */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + CL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CL */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + CO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CO */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + CR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CR */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + CZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CZ */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 22 + */ + display_priority: number; + }[]; + }; + DE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DE */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /MiVcYLkztM6qqLeVSYWHFCUcXx.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 30 + */ + provider_id: number; + /** @example WOW */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + DK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DK */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + DO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DO */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + DZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DZ */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + EC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EC */ + link?: string; + flatrate?: { + /** @example /cDzkhgvozSr4GW2aRdV22uDuFpw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 339 + */ + provider_id: number; + /** @example Movistar Play */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + EG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EG */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + ES?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ES */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + FI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FI */ + link?: string; + buy?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + FR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FR */ + link?: string; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /loOaayvNiLnD0zKl70TO2L5vlAL.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1870 + */ + provider_id: number; + /** @example Pass Warner Amazon Channel */ + provider_name?: string; + /** + * @default 0 + * @example 95 + */ + display_priority: number; + }[]; + }; + GB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GB */ + link?: string; + flatrate?: { + /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 29 + */ + provider_id: number; + /** @example Sky Go */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + GF?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GF */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + GH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GH */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + GQ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GQ */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + GT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GT */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + HK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HK */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 40 + */ + display_priority: number; + }[]; + }; + HN?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HN */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + HR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HR */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 34 + */ + display_priority: number; + }[]; + }; + HU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HU */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 22 + */ + display_priority: number; + }[]; + }; + ID?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ID */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 14 + */ + display_priority: number; + }[]; + }; + IE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IE */ + link?: string; + flatrate?: { + /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 29 + */ + provider_id: number; + /** @example Sky Go */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + buy?: { + /** @example /2pCbao1J9s0DMak2KKnEzmzHni8.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 130 + */ + provider_id: number; + /** @example Sky Store */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + IL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IL */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + }; + IQ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IQ */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + IT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IT */ + link?: string; + buy?: { + /** @example /cksgBjTHV3rzAVaO2zUyS1mH4Ke.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 40 + */ + provider_id: number; + /** @example Chili */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 29 + */ + provider_id: number; + /** @example Sky Go */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + JM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JM */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + JP?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JP */ + link?: string; + flatrate?: { + /** @example /npg1OiBidQSndMsBZwgEPOYU6Jq.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 84 + */ + provider_id: number; + /** @example U-NEXT */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + buy?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + rent?: { + /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + KE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KE */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + KR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KR */ + link?: string; + flatrate?: { + /** @example /2ioan5BX5L9tz4fIGU93blTeFhv.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 356 + */ + provider_id: number; + /** @example wavve */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + LB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LB */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + }; + LT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LT */ + link?: string; + flatrate?: { + /** @example /xTVM8uXT9QocigQ07LE7Irc65W2.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 553 + */ + provider_id: number; + /** @example Telia Play */ + provider_name?: string; + /** + * @default 0 + * @example 15 + */ + display_priority: number; + }[]; + }; + LY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LY */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + MD?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MD */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 26 + */ + display_priority: number; + }[]; + }; + MK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MK */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + }; + MU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MU */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + MX?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MX */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + MY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MY */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 14 + */ + display_priority: number; + }[]; + }; + MZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MZ */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + NE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NE */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + NG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NG */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + NL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NL */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 47 + */ + display_priority: number; + }[]; + buy?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + NO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NO */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + buy?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + NZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NZ */ + link?: string; + flatrate?: { + /** @example /od4YNSSLgOP3p8EtQTnEYfrPa77.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 273 + */ + provider_id: number; + /** @example Neon TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + PA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PA */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + PE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PE */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + PH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PH */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + PL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PL */ + link?: string; + flatrate?: { + /** @example /l5Wxbsgral716BOtZsGyPVNn8GC.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 250 + */ + provider_id: number; + /** @example Horizon */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + rent?: { + /** @example /bZNXgd8fwVTD68aAGlElkpAtu7b.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 549 + */ + provider_id: number; + /** @example IPLA */ + provider_name?: string; + /** + * @default 0 + * @example 17 + */ + display_priority: number; + }[]; + }; + PS?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PS */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + PT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PT */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + PY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PY */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + RO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RO */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 17 + */ + display_priority: number; + }[]; + }; + RS?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RS */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 32 + */ + display_priority: number; + }[]; + }; + RU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RU */ + link?: string; + flatrate?: { + /** @example /w1T8s7FqakcfucR8cgOvbe6UeXN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 115 + */ + provider_id: number; + /** @example Okko */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + ads?: { + /** @example /3jJtMOIwtvcrCyeRMUvv4wsfhJk.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 577 + */ + provider_id: number; + /** @example TvIgle */ + provider_name?: string; + /** + * @default 0 + * @example 22 + */ + display_priority: number; + }[]; + }; + SA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SA */ + link?: string; + flatrate?: { + /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + SC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SC */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + SE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SE */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + buy?: { + /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 68 + */ + provider_id: number; + /** @example Microsoft Store */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + SG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SG */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + }; + SI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SI */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + }; + SK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SK */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 37 + */ + display_priority: number; + }[]; + }; + SN?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SN */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + SV?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SV */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + TH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TH */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + TR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TR */ + link?: string; + flatrate?: { + /** @example /z3XAGCCbDD3KTZFvc96Ytr3XR56.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 341 + */ + provider_id: number; + /** @example blutv */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + }; + TT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TT */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + TW?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TW */ + link?: string; + flatrate?: { + /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 425 + */ + provider_id: number; + /** @example HBO Go */ + provider_name?: string; + /** + * @default 0 + * @example 40 + */ + display_priority: number; + }[]; + }; + TZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TZ */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + UG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UG */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + US?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=US */ + link?: string; + free?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + buy?: { + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + UY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UY */ + link?: string; + flatrate?: { + /** @example /kV8XFGI5OLJKl72dI8DtnKplfFr.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 467 + */ + provider_id: number; + /** @example DIRECTV GO */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + VE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=VE */ + link?: string; + flatrate?: { + /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 384 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + ZA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZA */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + ZM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZM */ + link?: string; + flatrate?: { + /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + }; + }; + }; + }; + }; + }; + 'tv-series-add-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header: { + 'Content-Type': string; + }; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + }; + }; + 'tv-series-delete-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header?: { + 'Content-Type'?: string; + }; + path: { + series_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 13 + */ + status_code: number; + /** @example The item/record was deleted successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'tv-season-details': { + parameters: { + query?: { + /** @description comma separated list of endpoints within this namespace, 20 items max */ + append_to_response?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example 5256c89f19c2956ff6046d47 */ + _id?: string; + /** @example 2011-04-17 */ + air_date?: string; + episodes?: { + /** @example 2011-04-17 */ + air_date?: string; + /** + * @default 0 + * @example 1 + */ + episode_number: number; + /** @example standard */ + episode_type?: string; + /** + * @default 0 + * @example 63056 + */ + id: number; + /** @example Winter Is Coming */ + name?: string; + /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ + overview?: string; + /** @example 101 */ + production_code?: string; + /** + * @default 0 + * @example 62 + */ + runtime: number; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** + * @default 0 + * @example 1399 + */ + show_id: number; + /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ + still_path?: string; + /** + * @default 0 + * @example 8.1 + */ + vote_average: number; + /** + * @default 0 + * @example 396 + */ + vote_count: number; + crew?: { + /** @example Directing */ + department?: string; + /** @example Director */ + job?: string; + /** @example 5256c8a219c2956ff6046e77 */ + credit_id?: string; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 44797 + */ + id: number; + /** @example Directing */ + known_for_department?: string; + /** @example Tim Van Patten */ + name?: string; + /** @example Tim Van Patten */ + original_name?: string; + /** + * @default 0 + * @example 0.8004 + */ + popularity: number; + /** @example /vwcARZBg4PEzOwnPsXdjRWeUVrZ.jpg */ + profile_path?: string; + }[]; + guest_stars?: { + /** @example Benjen Stark */ + character?: string; + /** @example 5256c8b919c2956ff604836a */ + credit_id?: string; + /** + * @default 0 + * @example 61 + */ + order: number; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 119783 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Joseph Mawle */ + name?: string; + /** @example Joseph Mawle */ + original_name?: string; + /** + * @default 0 + * @example 0.8932 + */ + popularity: number; + /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ + profile_path?: string; + }[]; + }[]; + /** @example Season 1 */ + name?: string; + networks?: { + /** + * @default 0 + * @example 49 + */ + id: number; + /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ + logo_path?: string; + /** @example HBO */ + name?: string; + /** @example US */ + origin_country?: string; + }[]; + /** @example Trouble is brewing in the Seven Kingdoms of Westeros. For the driven inhabitants of this visionary world, control of Westeros' Iron Throne holds the lure of great power. But in a land where the seasons can last a lifetime, winter is coming...and beyond the Great Wall that protects them, an ancient evil has returned. In Season One, the story centers on three primary areas: the Stark and the Lannister families, whose designs on controlling the throne threaten a tenuous peace; the dragon princess Daenerys, heir to the former dynasty, who waits just over the Narrow Sea with her malevolent brother Viserys; and the Great Wall--a massive barrier of ice where a forgotten danger is stirring. */ + overview?: string; + /** + * @default 0 + * @example 3624 + */ + id: number; + /** @example /wgfKiqzuMrFIkU1M68DDDY8kGC1.jpg */ + poster_path?: string; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** + * @default 0 + * @example 8.4 + */ + vote_average: number; + }; + }; + }; + }; + }; + 'tv-season-account-states': { + parameters: { + query?: { + session_id?: string; + guest_session_id?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + results?: { + /** + * @default 0 + * @example 63056 + */ + id: number; + /** + * @default 0 + * @example 1 + */ + episode_number: number; + rated?: { + /** + * @default 0 + * @example 9 + */ + value: number; + }; + }[]; + }; + }; + }; + }; + }; + 'tv-season-aggregate-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 22970 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Peter Dinklage */ + name?: string; + /** @example Peter Dinklage */ + original_name?: string; + /** + * @default 0 + * @example 30.6 + */ + popularity: number; + /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ + profile_path?: string; + roles?: { + /** @example 5256c8b219c2956ff6047cd8 */ + credit_id?: string; + /** @example Tyrion Lannister */ + character?: string; + /** + * @default 0 + * @example 10 + */ + episode_count: number; + }[]; + /** + * @default 0 + * @example 10 + */ + total_episode_count: number; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 1 + */ + gender: number; + /** + * @default 0 + * @example 9153 + */ + id: number; + /** @example Art */ + known_for_department?: string; + /** @example Gemma Jackson */ + name?: string; + /** @example Gemma Jackson */ + original_name?: string; + /** + * @default 0 + * @example 0.995 + */ + popularity: number; + profile_path?: unknown; + jobs?: { + /** @example 54eee8b8c3a3686d5e005430 */ + credit_id?: string; + /** @example Production Design */ + job?: string; + /** + * @default 0 + * @example 10 + */ + episode_count: number; + }[]; + /** @example Art */ + department?: string; + /** + * @default 0 + * @example 10 + */ + total_episode_count: number; + }[]; + /** + * @default 0 + * @example 3624 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-season-changes-by-id': { + parameters: { + query?: { + end_date?: string; + page?: number; + start_date?: string; + }; + header?: never; + path: { + season_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + changes?: { + /** @example episode */ + key?: string; + items?: { + /** @example 5717c8c69251414cfd00250f */ + id?: string; + /** @example updated */ + action?: string; + /** @example 2016-04-20 18:21:58 UTC */ + time?: string; + value?: { + /** + * @default 0 + * @example 63056 + */ + episode_id: number; + /** + * @default 0 + * @example 1 + */ + episode_number: number; + }; + }[]; + }[]; + }; + }; + }; + }; + }; + 'tv-season-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 22970 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Peter Dinklage */ + name?: string; + /** @example Peter Dinklage */ + original_name?: string; + /** + * @default 0 + * @example 30.6 + */ + popularity: number; + /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ + profile_path?: string; + /** @example Tyrion Lannister */ + character?: string; + /** @example 5256c8b219c2956ff6047cd8 */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 0 + */ + gender: number; + /** + * @default 0 + * @example 1223796 + */ + id: number; + /** @example Production */ + known_for_department?: string; + /** @example Frank Doelger */ + name?: string; + /** @example Frank Doelger */ + original_name?: string; + /** + * @default 0 + * @example 0.694 + */ + popularity: number; + profile_path?: unknown; + /** @example 5256c8c419c2956ff604867c */ + credit_id?: string; + /** @example Production */ + department?: string; + /** @example Producer */ + job?: string; + }[]; + /** + * @default 0 + * @example 3624 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-season-external-ids': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + /** @example /m/0gmd1gd */ + freebase_mid?: string; + /** @example /m/0gmd1gd */ + freebase_id?: string; + /** + * @default 0 + * @example 364731 + */ + tvdb_id: number; + tvrage_id?: unknown; + /** @example Q1658029 */ + wikidata_id?: string; + }; + }; + }; + }; + }; + 'tv-season-images': { + parameters: { + query?: { + /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ + include_image_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + posters?: { + /** + * @default 0 + * @example 0.667 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 1500 + */ + height: number; + /** @example en */ + iso_639_1?: string; + /** @example /wgfKiqzuMrFIkU1M68DDDY8kGC1.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.514 + */ + vote_average: number; + /** + * @default 0 + * @example 18 + */ + vote_count: number; + /** + * @default 0 + * @example 1000 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'tv-season-translations': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + translations?: { + /** @example SA */ + iso_3166_1?: string; + /** @example ar */ + iso_639_1?: string; + /** @example العربية */ + name?: string; + /** @example Arabic */ + english_name?: string; + data?: { + /** @example */ + name?: string; + /** @example سلسلة درامية مبنية على سلسلة روايات لـ جورج آر آر مارتن بعنوان "إيه سونغ أوف آيس أن فاير" والتي حققت مبيعات كبيرة وتتمحور حول الصراعات التي كانت تحدث في العصور الوسطى بين العائلات النبيلة للسيطرة على عرش وستيروس. */ + overview?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'tv-season-videos': { + parameters: { + query?: { + /** @description filter the list results by language, supports more than one value by using a comma */ + include_video_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + results?: { + /** @example en */ + iso_639_1?: string; + /** @example US */ + iso_3166_1?: string; + /** @example Game Of Thrones - Season 1 Recap - Official HBO UK */ + name?: string; + /** @example e0Y8KpQpW8c */ + key?: string; + /** @example YouTube */ + site?: string; + /** + * @default 0 + * @example 1080 + */ + size: number; + /** @example Recap */ + type?: string; + /** + * @default true + * @example true + */ + official: boolean; + /** @example 2015-05-19T16:31:23.000Z */ + published_at?: string; + /** @example 5ce71a920e0a265ac0cfe497 */ + id?: string; + }[]; + }; + }; + }; + }; + }; + 'tv-season-watch-providers': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + results?: { + AD?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AD */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 24 + */ + display_priority: number; + }[]; + }; + AE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AE */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + AG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AG */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + AR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AR */ + link?: string; + flatrate?: { + /** @example /nr5UBW4IGKgBwmhpTMOfcvnX2vX.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 467 + */ + provider_id: number; + /** @example DIRECTV GO */ + provider_name?: string; + /** + * @default 0 + * @example 12 + */ + display_priority: number; + }[]; + }; + AT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AT */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /kAZkQcIxMxTmlwdgSB05fqtymp0.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 29 + */ + provider_id: number; + /** @example Sky Go */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + }; + AU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AU */ + link?: string; + flatrate?: { + /** @example /fejdSG7TwNQ5E0p6u7A6LVs280R.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 134 + */ + provider_id: number; + /** @example Foxtel Now */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + buy?: { + /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + BA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BA */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + BB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BB */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 23 + */ + display_priority: number; + }[]; + }; + BE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BE */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 33 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /kwftIxtjuCAROIcdd53UEjzSmca.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1857 + */ + provider_id: number; + /** @example Telenet */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + BG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BG */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 21 + */ + display_priority: number; + }[]; + }; + BH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BH */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + }; + BO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BO */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + BR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BR */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + BS?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BS */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 24 + */ + display_priority: number; + }[]; + }; + BZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BZ */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + CA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CA */ + link?: string; + flatrate?: { + /** @example /ewOptMVIYcOadMGGJz8DJueH2bH.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 230 + */ + provider_id: number; + /** @example Crave */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + buy?: { + /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + CH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CH */ + link?: string; + buy?: { + /** @example /8z7rC8uIDaTM91X0ZfkRf04ydj2.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 3 + */ + provider_id: number; + /** @example Google Play Movies */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /ytApMa9fThUQUFTn696AeNBrB8f.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 210 + */ + provider_id: number; + /** @example Sky */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + CI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CI */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 19 + */ + display_priority: number; + }[]; + }; + CL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CL */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + CM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CM */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + CO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CO */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + CR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CR */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + CZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CZ */ + link?: string; + flatrate?: { + /** @example /489t5n9o1KhH7voGNQkrXT7vBKV.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1939 + */ + provider_id: number; + /** @example Lepsi TV */ + provider_name?: string; + /** + * @default 0 + * @example 26 + */ + display_priority: number; + }[]; + }; + DE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DE */ + link?: string; + flatrate?: { + /** @example /kAZkQcIxMxTmlwdgSB05fqtymp0.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 29 + */ + provider_id: number; + /** @example Sky Go */ + provider_name?: string; + /** + * @default 0 + * @example 8 + */ + display_priority: number; + }[]; + buy?: { + /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + DK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DK */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 39 + */ + display_priority: number; + }[]; + }; + DO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DO */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + EC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EC */ + link?: string; + flatrate?: { + /** @example /tRNA2CRgA4XHvd7Mx9dH3sFtDVb.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 339 + */ + provider_id: number; + /** @example MovistarTV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + EG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EG */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 20 + */ + display_priority: number; + }[]; + }; + ES?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ES */ + link?: string; + flatrate?: { + /** @example /f6TRLB3H4jDpFEZ0z2KWSSvu1SB.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 149 + */ + provider_id: number; + /** @example Movistar Plus+ Ficción Total */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 32 + */ + display_priority: number; + }[]; + }; + FI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FI */ + link?: string; + flatrate?: { + /** @example /eglAxQEXSO13p6gNf3HKymrIu7y.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 540 + */ + provider_id: number; + /** @example Elisa Viihde */ + provider_name?: string; + /** + * @default 0 + * @example 18 + */ + display_priority: number; + }[]; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 54 + */ + display_priority: number; + }[]; + }; + FR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FR */ + link?: string; + buy?: { + /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 78 + */ + display_priority: number; + }[]; + }; + GB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GB */ + link?: string; + buy?: { + /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 39 + */ + provider_id: number; + /** @example Now TV */ + provider_name?: string; + /** + * @default 0 + * @example 43 + */ + display_priority: number; + }[]; + }; + GG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GG */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 84 + */ + display_priority: number; + }[]; + }; + GQ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GQ */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + GT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GT */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + GY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GY */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + HK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HK */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 38 + */ + display_priority: number; + }[]; + }; + HN?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HN */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + HR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HR */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 33 + */ + display_priority: number; + }[]; + }; + HU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HU */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + ID?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ID */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 39 + */ + display_priority: number; + }[]; + }; + IE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IE */ + link?: string; + flatrate?: { + /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 39 + */ + provider_id: number; + /** @example Now TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + buy?: { + /** @example /6AKbY2ayaEuH4zKg2prqoVQ9iaY.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 130 + */ + provider_id: number; + /** @example Sky Store */ + provider_name?: string; + /** + * @default 0 + * @example 9 + */ + display_priority: number; + }[]; + }; + IN?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IN */ + link?: string; + flatrate?: { + /** @example /kVqjgpcwvDJOhCupjcLzwwtOp52.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 2336 + */ + provider_id: number; + /** @example JioHotstar */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + IQ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IQ */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + IT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IT */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 33 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 39 + */ + provider_id: number; + /** @example Now TV */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + JM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JM */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 22 + */ + display_priority: number; + }[]; + }; + JO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JO */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + JP?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JP */ + link?: string; + flatrate?: { + /** @example /a5T7vNaGvoeckYO6rQkHolvyYf4.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 84 + */ + provider_id: number; + /** @example U-NEXT */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + }[]; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + rent?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + KE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KE */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + LB?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LB */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + LC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LC */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + MC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MC */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + MD?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MD */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 25 + */ + display_priority: number; + }[]; + }; + ME?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ME */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + MG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MG */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + MK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MK */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 26 + */ + display_priority: number; + }[]; + }; + ML?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ML */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + MU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MU */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + MX?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MX */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 5 + */ + display_priority: number; + }[]; + }; + MY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MY */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + MZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MZ */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + NE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NE */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 19 + */ + display_priority: number; + }[]; + }; + NG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NG */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 21 + */ + display_priority: number; + }[]; + }; + NI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NI */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 13 + */ + display_priority: number; + }[]; + }; + NL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NL */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 34 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 48 + */ + display_priority: number; + }[]; + }; + NO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NO */ + link?: string; + flatrate?: { + /** @example /3ZigBD8WTEPcEHAvMWiJGUsv5u4.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 578 + */ + provider_id: number; + /** @example Strim */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + NZ?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NZ */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 59 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /iscLKFDwQlr0BAgVDBcuRapLiwC.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 273 + */ + provider_id: number; + /** @example Neon TV */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + OM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=OM */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + PA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PA */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + PE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PE */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + PH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PH */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 32 + */ + display_priority: number; + }[]; + }; + PL?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PL */ + link?: string; + flatrate?: { + /** @example /jhMNVBV2UocEGepRkr9oFPD7Gpb.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 505 + */ + provider_id: number; + /** @example Player */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + PT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PT */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 43 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 32 + */ + display_priority: number; + }[]; + }; + PY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PY */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + }; + QA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=QA */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + RO?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RO */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 23 + */ + display_priority: number; + }[]; + }; + RS?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RS */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + RU?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RU */ + link?: string; + flatrate?: { + /** @example /5z8dpQN27kybhn21EVLZcJPpMEo.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 115 + */ + provider_id: number; + /** @example Okko */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + SA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SA */ + link?: string; + flatrate?: { + /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 629 + */ + provider_id: number; + /** @example OSN+ */ + provider_name?: string; + /** + * @default 0 + * @example 20 + */ + display_priority: number; + }[]; + }; + SC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SC */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + SE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SE */ + link?: string; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 39 + */ + display_priority: number; + }[]; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 44 + */ + display_priority: number; + }[]; + }; + SG?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SG */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 31 + */ + display_priority: number; + }[]; + }; + SI?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SI */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 28 + */ + display_priority: number; + }[]; + }; + SK?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SK */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 37 + */ + display_priority: number; + }[]; + }; + SN?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SN */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + SV?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SV */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + TC?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TC */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 10 + */ + display_priority: number; + }[]; + }; + TD?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TD */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + TH?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TH */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 30 + */ + display_priority: number; + }[]; + }; + TR?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TR */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 29 + */ + display_priority: number; + }[]; + }; + TT?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TT */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + }; + TW?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TW */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 38 + */ + display_priority: number; + }[]; + }; + US?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=US */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 11 + */ + display_priority: number; + }[]; + buy?: { + /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 10 + */ + provider_id: number; + /** @example Amazon Video */ + provider_name?: string; + /** + * @default 0 + * @example 6 + */ + display_priority: number; + }[]; + }; + UY?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UY */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 3 + */ + display_priority: number; + }[]; + }; + VE?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=VE */ + link?: string; + flatrate?: { + /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 1899 + */ + provider_id: number; + /** @example HBO Max */ + provider_name?: string; + /** + * @default 0 + * @example 27 + */ + display_priority: number; + }[]; + }; + ZA?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZA */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 4 + */ + display_priority: number; + }[]; + }; + ZM?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZM */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 7 + */ + display_priority: number; + }[]; + }; + ZW?: { + /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZW */ + link?: string; + flatrate?: { + /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ + logo_path?: string; + /** + * @default 0 + * @example 55 + */ + provider_id: number; + /** @example ShowMax */ + provider_name?: string; + /** + * @default 0 + * @example 0 + */ + display_priority: number; + }[]; + }; + }; + }; + }; + }; + }; + }; + 'tv-episode-details': { + parameters: { + query?: { + /** @description comma separated list of endpoints within this namespace, 20 items max */ + append_to_response?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example 2011-04-17 */ + air_date?: string; + crew?: { + /** @example Directing */ + department?: string; + /** @example Director */ + job?: string; + /** @example 5256c8a219c2956ff6046e77 */ + credit_id?: string; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 44797 + */ + id: number; + /** @example Directing */ + known_for_department?: string; + /** @example Timothy Van Patten */ + name?: string; + /** @example Timothy Van Patten */ + original_name?: string; + /** + * @default 0 + * @example 7.775 + */ + popularity: number; + /** @example /MzSOFrd99HRdr6pkSRSctk3kBR.jpg */ + profile_path?: string; + }[]; + /** + * @default 0 + * @example 1 + */ + episode_number: number; + guest_stars?: { + /** @example Benjen Stark */ + character?: string; + /** @example 5256c8b919c2956ff604836a */ + credit_id?: string; + /** + * @default 0 + * @example 62 + */ + order: number; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 119783 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Joseph Mawle */ + name?: string; + /** @example Joseph Mawle */ + original_name?: string; + /** + * @default 0 + * @example 6.758 + */ + popularity: number; + /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ + profile_path?: string; + }[]; + /** @example Winter Is Coming */ + name?: string; + /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ + overview?: string; + /** + * @default 0 + * @example 63056 + */ + id: number; + /** @example 101 */ + production_code?: string; + /** + * @default 0 + * @example 62 + */ + runtime: number; + /** + * @default 0 + * @example 1 + */ + season_number: number; + /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ + still_path?: string; + /** + * @default 0 + * @example 7.8 + */ + vote_average: number; + /** + * @default 0 + * @example 286 + */ + vote_count: number; + }; + }; + }; + }; + }; + 'tv-episode-account-states': { + parameters: { + query?: { + session_id?: string; + guest_session_id?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 550 + */ + id: number; + /** + * @default true + * @example true + */ + favorite: boolean; + rated?: { + /** + * @default 0 + * @example 9 + */ + value: number; + }; + /** + * @default true + * @example false + */ + watchlist: boolean; + }; + }; + }; + }; + }; + 'tv-episode-changes-by-id': { + parameters: { + query?: never; + header?: never; + path: { + episode_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + changes?: { + /** @example production_code */ + key?: string; + items?: { + /** @example 54bd9ed7c3a3686c6b00da66 */ + id?: string; + /** @example added */ + action?: string; + /** @example 2015-01-20 00:18:31 UTC */ + time?: string; + /** @example 101 */ + value?: string; + }[]; + }[]; + }; + }; + }; + }; + }; + 'tv-episode-credits': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cast?: { + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 22970 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Peter Dinklage */ + name?: string; + /** @example Peter Dinklage */ + original_name?: string; + /** + * @default 0 + * @example 30.6 + */ + popularity: number; + /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ + profile_path?: string; + /** @example Tyrion Lannister */ + character?: string; + /** @example 5256c8b219c2956ff6047cd8 */ + credit_id?: string; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + crew?: { + /** @example Directing */ + department?: string; + /** @example Director */ + job?: string; + /** @example 5256c8a219c2956ff6046e77 */ + credit_id?: string; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 44797 + */ + id: number; + /** @example Directing */ + known_for_department?: string; + /** @example Timothy Van Patten */ + name?: string; + /** @example Timothy Van Patten */ + original_name?: string; + /** + * @default 0 + * @example 8.292 + */ + popularity: number; + /** @example /MzSOFrd99HRdr6pkSRSctk3kBR.jpg */ + profile_path?: string; + }[]; + guest_stars?: { + /** @example Benjen Stark */ + character?: string; + /** @example 5256c8b919c2956ff604836a */ + credit_id?: string; + /** + * @default 0 + * @example 62 + */ + order: number; + /** + * @default true + * @example false + */ + adult: boolean; + /** + * @default 0 + * @example 2 + */ + gender: number; + /** + * @default 0 + * @example 119783 + */ + id: number; + /** @example Acting */ + known_for_department?: string; + /** @example Joseph Mawle */ + name?: string; + /** @example Joseph Mawle */ + original_name?: string; + /** + * @default 0 + * @example 8.559 + */ + popularity: number; + /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ + profile_path?: string; + }[]; + /** + * @default 0 + * @example 63056 + */ + id: number; + }; + }; + }; + }; + }; + 'tv-episode-external-ids': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 63056 + */ + id: number; + /** @example tt1480055 */ + imdb_id?: string; + /** @example /m/0gmc6ph */ + freebase_mid?: string; + /** @example /en/winter_is_coming */ + freebase_id?: string; + /** + * @default 0 + * @example 3254641 + */ + tvdb_id: number; + /** + * @default 0 + * @example 1065008299 + */ + tvrage_id: number; + /** @example Q2614622 */ + wikidata_id?: string; + }; + }; + }; + }; + }; + 'tv-episode-images': { + parameters: { + query?: { + /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ + include_image_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 63056 + */ + id: number; + stills?: { + /** + * @default 0 + * @example 1.778 + */ + aspect_ratio: number; + /** + * @default 0 + * @example 1080 + */ + height: number; + iso_639_1?: unknown; + /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ + file_path?: string; + /** + * @default 0 + * @example 5.454 + */ + vote_average: number; + /** + * @default 0 + * @example 3 + */ + vote_count: number; + /** + * @default 0 + * @example 1920 + */ + width: number; + }[]; + }; + }; + }; + }; + }; + 'tv-episode-translations': { + parameters: { + query?: never; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 63056 + */ + id: number; + translations?: { + /** @example SA */ + iso_3166_1?: string; + /** @example ar */ + iso_639_1?: string; + /** @example العربية */ + name?: string; + /** @example Arabic */ + english_name?: string; + data?: { + /** @example */ + name?: string; + /** @example خلف باب واسع من الجليد في شمالي وستيروس هناك شيء يحدث. تتلقى عائلة ستارك التي من وينترفيل زيارة من العائلة المالكة، بينما يشكل أمير عائلة تارغارين المنفي تحالفاً جديداً للسيطرة على العرش من جديد. */ + overview?: string; + }; + }[]; + }; + }; + }; + }; + }; + 'tv-episode-videos': { + parameters: { + query?: { + /** @description filter the list results by language, supports more than one value by using a comma */ + include_video_language?: string; + language?: string; + }; + header?: never; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 3624 + */ + id: number; + results?: { + /** @example en */ + iso_639_1?: string; + /** @example US */ + iso_3166_1?: string; + /** @example Game Of Thrones - Season 1 Recap - Official HBO UK */ + name?: string; + /** @example e0Y8KpQpW8c */ + key?: string; + /** @example YouTube */ + site?: string; + /** + * @default 0 + * @example 1080 + */ + size: number; + /** @example Recap */ + type?: string; + /** + * @default true + * @example true + */ + official: boolean; + /** @example 2015-05-19T16:31:23.000Z */ + published_at?: string; + /** @example 5ce71a920e0a265ac0cfe497 */ + id?: string; + }[]; + }; + }; + }; + }; + }; + 'tv-episode-add-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header: { + 'Content-Type': string; + }; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 1 + */ + status_code: number; + /** @example Success. */ + status_message?: string; + }; + }; + }; + }; + }; + 'tv-episode-delete-rating': { + parameters: { + query?: { + guest_session_id?: string; + session_id?: string; + }; + header?: { + 'Content-Type'?: string; + }; + path: { + series_id: number; + season_number: number; + episode_number: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** + * @default 0 + * @example 13 + */ + status_code: number; + /** @example The item/record was deleted successfully. */ + status_message?: string; + }; + }; + }; + }; + }; + 'tv-episode-group-details': { + parameters: { + query?: never; + header?: never; + path: { + tv_episode_group_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example Comedians in Cars organized in Netflix's collections. */ + description?: string; + /** + * @default 0 + * @example 83 + */ + episode_count: number; + /** + * @default 0 + * @example 6 + */ + group_count: number; + groups?: { + /** @example 5acf93efc3a368739a0000a9 */ + id?: string; + /** @example First Cup */ + name?: string; + /** + * @default 0 + * @example 1 + */ + order: number; + episodes?: { + /** @example 2015-06-17 */ + air_date?: string; + /** + * @default 0 + * @example 3 + */ + episode_number: number; + /** + * @default 0 + * @example 1078262 + */ + id: number; + /** @example Jim Carrey: We Love Breathing What You're Burning, Baby */ + name?: string; + /** @example Jerry’s full of testosterone as he steps into a ‘76 Lamborghini Countach with Jim Carrey, who’s between a three-week cleanse and a five-day silent retreat. After coffee, it’s off to Carrey’s studio to study a portrait of a gorilla with a machine gun. Wow. */ + overview?: string; + /** @example */ + production_code?: string; + runtime?: unknown; + /** + * @default 0 + * @example 6 + */ + season_number: number; + /** + * @default 0 + * @example 59717 + */ + show_id: number; + /** @example /aOyE420zuFq9zWtEWjIccAiTrzU.jpg */ + still_path?: string; + /** + * @default 0 + * @example 7.4 + */ + vote_average: number; + /** + * @default 0 + * @example 5 + */ + vote_count: number; + /** + * @default 0 + * @example 0 + */ + order: number; + }[]; + /** + * @default true + * @example true + */ + locked: boolean; + }[]; + /** @example 5acf93e60e0a26346d0000ce */ + id?: string; + /** @example Netflix Collections */ + name?: string; + network?: { + /** + * @default 0 + * @example 213 + */ + id: number; + /** @example /wwemzKWzjKYJFfCeiB57q3r4Bcm.png */ + logo_path?: string; + /** @example Netflix */ + name?: string; + /** @example */ + origin_country?: string; + }; + /** + * @default 0 + * @example 4 + */ + type: number; + }; + }; + }; + }; + }; + 'watch-providers-available-regions': { + parameters: { + query?: { + language?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + /** @example AD */ + iso_3166_1?: string; + /** @example Andorra */ + english_name?: string; + /** @example Andorra */ + native_name?: string; + }[]; + }; + }; + }; + }; + }; + 'watch-providers-movie-list': { + parameters: { + query?: { + language?: string; + watch_region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + display_priorities?: { + /** + * @default 0 + * @example 6 + */ + CA: number; + /** + * @default 0 + * @example 1 + */ + AE: number; + /** + * @default 0 + * @example 3 + */ + AR: number; + /** + * @default 0 + * @example 4 + */ + AT: number; + /** + * @default 0 + * @example 10 + */ + AU: number; + /** + * @default 0 + * @example 6 + */ + BE: number; + /** + * @default 0 + * @example 6 + */ + BO: number; + /** + * @default 0 + * @example 8 + */ + BR: number; + /** + * @default 0 + * @example 2 + */ + BG: number; + /** + * @default 0 + * @example 4 + */ + CH: number; + /** + * @default 0 + * @example 3 + */ + CL: number; + /** + * @default 0 + * @example 4 + */ + CO: number; + /** + * @default 0 + * @example 5 + */ + CR: number; + /** + * @default 0 + * @example 3 + */ + CZ: number; + /** + * @default 0 + * @example 4 + */ + DE: number; + /** + * @default 0 + * @example 7 + */ + DK: number; + /** + * @default 0 + * @example 7 + */ + EC: number; + /** + * @default 0 + * @example 3 + */ + EE: number; + /** + * @default 0 + * @example 2 + */ + EG: number; + /** + * @default 0 + * @example 4 + */ + ES: number; + /** + * @default 0 + * @example 10 + */ + FI: number; + /** + * @default 0 + * @example 5 + */ + FR: number; + /** + * @default 0 + * @example 5 + */ + GB: number; + /** + * @default 0 + * @example 2 + */ + GR: number; + /** + * @default 0 + * @example 7 + */ + GT: number; + /** + * @default 0 + * @example 5 + */ + HK: number; + /** + * @default 0 + * @example 7 + */ + HN: number; + /** + * @default 0 + * @example 3 + */ + HU: number; + /** + * @default 0 + * @example 4 + */ + ID: number; + /** + * @default 0 + * @example 4 + */ + IE: number; + /** + * @default 0 + * @example 8 + */ + IN: number; + /** + * @default 0 + * @example 4 + */ + IT: number; + /** + * @default 0 + * @example 7 + */ + JP: number; + /** + * @default 0 + * @example 3 + */ + LT: number; + /** + * @default 0 + * @example 3 + */ + LV: number; + /** + * @default 0 + * @example 4 + */ + MX: number; + /** + * @default 0 + * @example 4 + */ + MY: number; + /** + * @default 0 + * @example 8 + */ + NL: number; + /** + * @default 0 + * @example 6 + */ + NO: number; + /** + * @default 0 + * @example 4 + */ + NZ: number; + /** + * @default 0 + * @example 3 + */ + PE: number; + /** + * @default 0 + * @example 4 + */ + PH: number; + /** + * @default 0 + * @example 1 + */ + PL: number; + /** + * @default 0 + * @example 4 + */ + PT: number; + /** + * @default 0 + * @example 7 + */ + PY: number; + /** + * @default 0 + * @example 2 + */ + RU: number; + /** + * @default 0 + * @example 1 + */ + SA: number; + /** + * @default 0 + * @example 8 + */ + SE: number; + /** + * @default 0 + * @example 5 + */ + SG: number; + /** + * @default 0 + * @example 3 + */ + SK: number; + /** + * @default 0 + * @example 4 + */ + TH: number; + /** + * @default 0 + * @example 6 + */ + TR: number; + /** + * @default 0 + * @example 7 + */ + TW: number; + /** + * @default 0 + * @example 4 + */ + US: number; + /** + * @default 0 + * @example 4 + */ + VE: number; + /** + * @default 0 + * @example 2 + */ + ZA: number; + /** + * @default 0 + * @example 31 + */ + SI: number; + /** + * @default 0 + * @example 13 + */ + CV: number; + /** + * @default 0 + * @example 17 + */ + GH: number; + /** + * @default 0 + * @example 15 + */ + MU: number; + /** + * @default 0 + * @example 16 + */ + MZ: number; + /** + * @default 0 + * @example 16 + */ + UG: number; + /** + * @default 0 + * @example 28 + */ + IL: number; + }; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + }[]; + }; + }; + }; + }; + }; + 'watch-provider-tv-list': { + parameters: { + query?: { + language?: string; + watch_region?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 200 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + results?: { + display_priorities?: { + /** + * @default 0 + * @example 6 + */ + CA: number; + /** + * @default 0 + * @example 1 + */ + AE: number; + /** + * @default 0 + * @example 3 + */ + AR: number; + /** + * @default 0 + * @example 4 + */ + AT: number; + /** + * @default 0 + * @example 10 + */ + AU: number; + /** + * @default 0 + * @example 6 + */ + BE: number; + /** + * @default 0 + * @example 6 + */ + BO: number; + /** + * @default 0 + * @example 8 + */ + BR: number; + /** + * @default 0 + * @example 2 + */ + BG: number; + /** + * @default 0 + * @example 4 + */ + CH: number; + /** + * @default 0 + * @example 3 + */ + CL: number; + /** + * @default 0 + * @example 4 + */ + CO: number; + /** + * @default 0 + * @example 5 + */ + CR: number; + /** + * @default 0 + * @example 3 + */ + CZ: number; + /** + * @default 0 + * @example 4 + */ + DE: number; + /** + * @default 0 + * @example 7 + */ + DK: number; + /** + * @default 0 + * @example 7 + */ + EC: number; + /** + * @default 0 + * @example 3 + */ + EE: number; + /** + * @default 0 + * @example 2 + */ + EG: number; + /** + * @default 0 + * @example 4 + */ + ES: number; + /** + * @default 0 + * @example 10 + */ + FI: number; + /** + * @default 0 + * @example 5 + */ + FR: number; + /** + * @default 0 + * @example 5 + */ + GB: number; + /** + * @default 0 + * @example 2 + */ + GR: number; + /** + * @default 0 + * @example 7 + */ + GT: number; + /** + * @default 0 + * @example 5 + */ + HK: number; + /** + * @default 0 + * @example 7 + */ + HN: number; + /** + * @default 0 + * @example 3 + */ + HU: number; + /** + * @default 0 + * @example 4 + */ + ID: number; + /** + * @default 0 + * @example 4 + */ + IE: number; + /** + * @default 0 + * @example 8 + */ + IN: number; + /** + * @default 0 + * @example 4 + */ + IT: number; + /** + * @default 0 + * @example 7 + */ + JP: number; + /** + * @default 0 + * @example 3 + */ + LT: number; + /** + * @default 0 + * @example 3 + */ + LV: number; + /** + * @default 0 + * @example 4 + */ + MX: number; + /** + * @default 0 + * @example 4 + */ + MY: number; + /** + * @default 0 + * @example 8 + */ + NL: number; + /** + * @default 0 + * @example 6 + */ + NO: number; + /** + * @default 0 + * @example 4 + */ + NZ: number; + /** + * @default 0 + * @example 3 + */ + PE: number; + /** + * @default 0 + * @example 4 + */ + PH: number; + /** + * @default 0 + * @example 1 + */ + PL: number; + /** + * @default 0 + * @example 4 + */ + PT: number; + /** + * @default 0 + * @example 7 + */ + PY: number; + /** + * @default 0 + * @example 2 + */ + RU: number; + /** + * @default 0 + * @example 1 + */ + SA: number; + /** + * @default 0 + * @example 8 + */ + SE: number; + /** + * @default 0 + * @example 5 + */ + SG: number; + /** + * @default 0 + * @example 3 + */ + SK: number; + /** + * @default 0 + * @example 4 + */ + TH: number; + /** + * @default 0 + * @example 6 + */ + TR: number; + /** + * @default 0 + * @example 7 + */ + TW: number; + /** + * @default 0 + * @example 4 + */ + US: number; + /** + * @default 0 + * @example 4 + */ + VE: number; + /** + * @default 0 + * @example 2 + */ + ZA: number; + /** + * @default 0 + * @example 31 + */ + SI: number; + /** + * @default 0 + * @example 13 + */ + CV: number; + /** + * @default 0 + * @example 17 + */ + GH: number; + /** + * @default 0 + * @example 15 + */ + MU: number; + /** + * @default 0 + * @example 16 + */ + MZ: number; + /** + * @default 0 + * @example 16 + */ + UG: number; + /** + * @default 0 + * @example 28 + */ + IL: number; + }; + /** + * @default 0 + * @example 2 + */ + display_priority: number; + /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ + logo_path?: string; + /** @example Apple TV */ + provider_name?: string; + /** + * @default 0 + * @example 2 + */ + provider_id: number; + }[]; + }; + }; + }; + }; + }; +} diff --git a/main.ts b/main.ts new file mode 100644 index 00000000..8705b47c --- /dev/null +++ b/main.ts @@ -0,0 +1,1266 @@ +import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder, TFile } from 'obsidian'; +import { requestUrl, normalizePath } from 'obsidian'; +import { MediaType } from 'src/utils/MediaType'; +import { APIManager } from './api/APIManager'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './api/musicBrainzConstants'; +import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; +import { ComicVineAPI } from './api/apis/ComicVineAPI'; +import { GiantBombAPI } from './api/apis/GiantBombAPI'; +import { IGDBAPI } from './api/apis/IGDBAPI'; +import { MALAPI } from './api/apis/MALAPI'; +import { MALAPIManga } from './api/apis/MALAPIManga'; +import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; +import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; +import { MusicBrainzArtistAPI } from './api/apis/MusicBrainzArtistAPI'; +import { OMDbAPI } from './api/apis/OMDbAPI'; +import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; +import { RAWGAPI } from './api/apis/RAWGAPI'; +import { SteamAPI } from './api/apis/SteamAPI'; +import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; +import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; +import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; +import { VNDBAPI } from './api/apis/VNDBAPI'; +import { WikipediaAPI } from './api/apis/WikipediaAPI'; +import { GeniusClient } from './api/GeniusClient'; +import { SpotifyClient } from './api/SpotifyClient'; +import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; +import { BulkUpdateConfirmModal } from './modals/BulkUpdateConfirmModal'; +import { CompletionModal } from './modals/CompletionModal'; +import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; +import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; +import type { ArtistModel } from './models/ArtistModel'; +import type { MediaTypeModel } from './models/MediaTypeModel'; +import type { MusicReleaseModel } from './models/MusicReleaseModel'; +import type { SeasonModel } from './models/SeasonModel'; +import { SongModel } from './models/SongModel'; +import { ApiSecretID, getApiSecretValue } from './settings/apiSecretsHelper'; +import { PropertyMapper } from './settings/PropertyMapper'; +import { PropertyMappingModel } from './settings/PropertyMapping'; +import type { MediaDbPluginSettings } from './settings/Settings'; +import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOrder } from './settings/Settings'; +import { AutoTrackerHelper } from './utils/AutoTrackerHelper'; +import { BulkImportHelper } from './utils/BulkImportHelper'; +import { BulkUpdateHelper } from './utils/BulkUpdateHelper'; +import { DateFormatter } from './utils/DateFormatter'; +import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; +import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; +import type { SearchModalOptions } from './utils/ModalHelper'; +import { ModalHelper } from './utils/ModalHelper'; +import type { CreateNoteOptions } from './utils/Utils'; +import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; +import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable } from './utils/Utils'; +import 'src/styles.css'; + +export type Metadata = Record; + +export interface MediaTypeModelObj { + id: string; + type: MediaType; + dataSource: string; +} + +export default class MediaDbPlugin extends Plugin { + settings!: MediaDbPluginSettings; + apiManager!: APIManager; + mediaTypeManager!: MediaTypeManager; + modelPropertyMapper!: PropertyMapper; + modalHelper!: ModalHelper; + bulkImportHelper!: BulkImportHelper; + bulkUpdateHelper!: BulkUpdateHelper; + autoTrackerHelper!: AutoTrackerHelper; + dateFormatter!: DateFormatter; + + frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; + + async onload(): Promise { + this.apiManager = new APIManager(); + // register APIs + this.apiManager.registerAPI(new OMDbAPI(this)); + this.apiManager.registerAPI(new MALAPI(this)); + this.apiManager.registerAPI(new MALAPIManga(this)); + this.apiManager.registerAPI(new WikipediaAPI(this)); + this.apiManager.registerAPI(new MusicBrainzAPI(this)); + this.apiManager.registerAPI(new MusicBrainzArtistAPI(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)); + this.apiManager.registerAPI(new MobyGamesAPI(this)); + this.apiManager.registerAPI(new GiantBombAPI(this)); + this.apiManager.registerAPI(new IGDBAPI(this)); + this.apiManager.registerAPI(new RAWGAPI(this)); + this.apiManager.registerAPI(new VNDBAPI(this)); + + this.mediaTypeManager = new MediaTypeManager(); + this.modelPropertyMapper = new PropertyMapper(this); + this.modalHelper = new ModalHelper(this); + this.bulkImportHelper = new BulkImportHelper(this); + this.bulkUpdateHelper = new BulkUpdateHelper(this); + this.autoTrackerHelper = new AutoTrackerHelper(this); + this.dateFormatter = new DateFormatter(); + + await this.loadSettings(); + // register the settings tab + this.addSettingTab(new MediaDbSettingTab(this.app, this)); + + this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); + this.dateFormatter.setFormat(this.settings.customDateFormat); + + // add icon to the left ribbon and auto-tracker logic + this.refreshAutoTrackerRibbon(); + + this.app.workspace.onLayoutReady(() => { + if (this.settings.autoUpdateAiringMode) { + setTimeout(() => { + this.autoTrackerHelper.startBackgroundScan(true); + }, 5000); + } + }); + + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file) => { + if (file instanceof TFolder) { + // Add our customized context menu options under a "Media DB" group + menu.addItem(item => { + item.setTitle('Media DB...'); + item.setIcon('database'); + // @ts-ignore + if (typeof item.setSubmenu === 'function') { + // @ts-ignore + const sub = item.setSubmenu(); + sub.addItem((subItem: any) => subItem.setTitle('Bulk Import Folder').setIcon('database').onClick(() => this.bulkImportHelper.import(file))); + sub.addItem((subItem: any) => subItem.setTitle('Bulk Update Metadata').setIcon('refresh-cw').onClick(() => this.bulkUpdateHelper.updateFolder(file))); + sub.addItem((subItem: any) => subItem.setTitle('Start Auto-Tracker in Folder').setIcon('sync').onClick(() => { + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); + }).open(); + })); + sub.addItem((subItem: any) => subItem.setTitle('Download images in folder').setIcon('image').onClick(() => this.downloadImagesInFolder(file))); + } else { + // Fallback if setSubmenu isn't in older Obsidian versions + item.onClick(() => this.bulkUpdateHelper.updateFolder(file)); + } + }); + } + }), + ); + + this.addCommand({ + id: 'media-db-bulk-import-active-file-folder', + name: 'Media DB: Bulk Import Folder (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.bulkImportHelper.import(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-bulk-update-active-file-folder', + name: 'Media DB: Bulk Update Metadata (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.bulkUpdateHelper.updateFolder(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-download-images-active-file-folder', + name: 'Media DB: Download images in folder (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.downloadImagesInFolder(activeFile.parent); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-download-images-active-note', + name: 'Media DB: Download images in active note', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || activeFile.extension !== 'md') return false; + if (!checking) void this.downloadImagesInFile(activeFile); + return true; + }, + }); + + this.addCommand({ + id: 'media-db-manual-sync-auto-tracker', + name: 'Media DB: Force Auto-Tracker Background Scan', + callback: () => this.autoTrackerHelper.startBackgroundScan(false), + }); + + // register command to open search modal + this.addCommand({ + id: 'open-media-db-search-modal', + name: 'Create Media DB entry', + callback: () => this.createEntryWithSearchModal(), + }); + for (const mediaType of MEDIA_TYPES) { + this.addCommand({ + id: `open-media-db-search-modal-with-${mediaType}`, + name: `Create Media DB entry: ${unCamelCase(mediaType)}`, + callback: () => this.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), + }); + } + this.addCommand({ + id: 'open-media-db-advanced-search-modal', + name: 'Create Media DB entry (advanced search)', + callback: () => this.createEntryWithAdvancedSearchModal(), + }); + // register command to open id search modal + this.addCommand({ + id: 'open-media-db-id-search-modal', + name: 'Create Media DB entry by id', + callback: () => this.createEntryWithIdSearchModal(), + }); + // register command to update the open note + this.addCommand({ + id: 'update-media-db-note', + name: 'Update open note (this will recreate the note)', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.updateActiveNote(false); + } + return true; + }, + }); + this.addCommand({ + id: 'update-media-db-note-metadata', + name: 'Update metadata', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.updateActiveNote(true); + } + return true; + }, + }); + // register link insert command + this.addCommand({ + id: 'add-media-db-link', + name: 'Insert link', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.createLinkWithSearchModal(); + } + return true; + }, + }); + } + + async createLinkWithSearchModal(): Promise { + const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); + }); + + if (!apiSearchResults) { + return; + } + + const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + }); + + if (!selectResults || selectResults.length < 1) { + return; + } + + const link = `[${selectResults[0].title}](${selectResults[0].url})`; + + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + + // Make sure the user is editing a Markdown file. + if (view) { + view.editor.replaceRange(link, view.editor.getCursor()); + } + } + + async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { + let types: string[] = []; + let apiSearchResults = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { + types = searchModalData.types; + const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); + try { + return await this.apiManager.query(searchModalData.query, apis); + } catch (e) { + console.warn('MDB | Query failed:', e); + new Notice(`Search failed: ${e}`); + return []; + } + }); + + if (!apiSearchResults || apiSearchResults.length === 0) { + new Notice('No results found.'); + return; + } + + // filter the results + apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); + + if (apiSearchResults.length === 0) { + new Notice('No results found for the selected types.'); + return; + } + + // Show selection modal - for seasons, skip detail query + const selectResults = + types.length === 1 && types[0] === 'season' + ? await this.modalHelper.openSelectModal( + { + elements: apiSearchResults, + description: 'Select one search result to proceed.', + submitButtonText: 'Ok', + }, + async selectModalData => selectModalData.selected, + ) + : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected)); + + if (!selectResults || selectResults.length === 0) { + return; + } + + // Handle season selection for both direct season searches and series-to-season conversion + const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); + if (seasonHandlingResult.handled) { + return; + } + + // Show preview and confirm + const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => previewModalData.confirmed); + if (!confirmed) { + return; + } + + // User confirmed, create notes and exit + await this.createMediaDbNotes(selectResults); + } + + /** + * Handles the season workflow for both direct season searches and series-to-season conversion. + * Returns an object indicating what happened and how to proceed. + */ + private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { + // Case 1: User searched specifically for seasons and selected a series from TMDB + if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { + const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); + return { handled: true, seasonsCreated: created }; + } + + // Case 2: User searched for series but it's actually from TMDBSeasonAPI + // (This happens when searching for seasons returns series results) + if (types.includes('series') && selectResults.some(r => r.dataSource === 'TMDBSeriesAPI')) { + const seriesResults = selectResults.filter(r => r.dataSource === 'TMDBSeriesAPI'); + // If only one series result and user searched for seasons, show season selection + if (seriesResults.length === 1 && types.includes('season')) { + const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); + return { handled: true, seasonsCreated: created }; + } + } + + return { handled: false }; + } + + /** + * Shows the season selection modal for a given series and creates notes for selected seasons. + * Returns true if seasons were successfully created, false if cancelled. + */ + private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { + const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; + if (!tmdbSeasonAPI) { + new Notice('TMDBSeasonAPI not available.'); + return false; + } + + try { + // Fetch all seasons for the selected series + const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); + if (!allSeasons || allSeasons.length === 0) { + new Notice('No seasons found for this series.'); + return false; + } + + // Show season selection modal + const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); + if (!selectedSeasons || selectedSeasons.length === 0) { + return false; + } + + // Create notes for all selected seasons in parallel + await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); + new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); + return true; + } catch (e) { + console.warn('MDB | Error in season selection workflow:', e); + new Notice(`Error loading seasons: ${e}`); + return false; + } + } + + /** + * Shows the season selection modal and returns the selected seasons. + */ + private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { + const modal = new MediaDbSeasonSelectModal( + this, + allSeasons.map(s => ({ + season_number: s.seasonNumber, + name: s.seasonTitle || s.title, + episode_count: s.episodes || 0, + air_date: s.year > 0 ? String(s.year) : 'unknown', + poster_path: s.image, + })), + true, + seriesTitle, + ); + + return new Promise(resolve => { + modal.setSubmitCb(resolve); + modal.open(); + }); + } + + /** + * Creates notes for all selected seasons by fetching full metadata and creating entries. + */ + private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { + await Promise.all( + selectedSeasons.map(async selectedSeason => { + const seasonModel = allSeasons.find(s => s.seasonNumber === selectedSeason.season_number); + if (seasonModel) { + try { + // Fetch full metadata using getById + const fullMetadata = await tmdbSeasonAPI.getById(seasonModel.id); + await this.createMediaDbNotes([fullMetadata]); + } catch (e) { + console.warn(`MDB | Failed to create season ${selectedSeason.season_number}:`, e); + new Notice(`Failed to create season ${selectedSeason.season_number}: ${e}`); + } + } + }), + ); + } + + async createEntryWithAdvancedSearchModal(): Promise { + const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); + }); + + if (!apiSearchResults || apiSearchResults.length === 0) { + new Notice('No results found.'); + return; + } + + let selectResults: MediaTypeModel[]; + const proceed: boolean = false; + + while (!proceed) { + selectResults = + (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { + return await this.queryDetails(selectModalData.selected); + })) ?? []; + if (!selectResults || selectResults.length < 1) { + return; + } + + const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { + return previewModalData.confirmed; + }); + if (!confirmed) { + return; + } + break; + } + + await this.createMediaDbNotes(selectResults!); + } + + async createEntryWithIdSearchModal(): Promise { + let idSearchResult: MediaTypeModel | undefined = undefined; + let proceed: boolean = false; + + while (!proceed) { + idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => { + return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api); + }); + if (!idSearchResult) { + return; + } + + proceed = await this.modalHelper.openPreviewModal({ elements: [idSearchResult] }, async previewModalData => { + return previewModalData.confirmed; + }); + } + + if (!idSearchResult) { + return; + } + await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); + } + + async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { + const hasArtist = models.some(m => m.getMediaType() === MediaType.Artist); + + if (hasArtist) { + for (const model of models) { + await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); + } + return; + } + + const results = await Promise.allSettled(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }))); + + const failures = results.filter(r => r.status === 'rejected'); + if (failures.length > 0) { + console.warn('MDB | Some notes failed to create:', failures); + new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); + } + } + + async queryDetails(models: MediaTypeModel[]): Promise { + // Query details in parallel for better performance + const results = await Promise.allSettled(models.map(model => this.apiManager.queryDetailedInfo(model))); + + // Filter out failures and return successful results + const detailModels: MediaTypeModel[] = results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled' && r.value !== undefined) + .map(r => r.value!); + + // Log failures for debugging + const failures = results.filter(r => r.status === 'rejected'); + if (failures.length > 0) { + console.warn('MDB | Some detail queries failed:', failures); + } + + return detailModels; + } + + async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + if (mediaTypeModel.getMediaType() === MediaType.Artist) { + await this.importArtistDiscography(mediaTypeModel as ArtistModel, options); + return; + } + + await this.createStandardMediaDbNoteFromModel(mediaTypeModel, options); + } + + /** @returns whether the note file was created (false if the user cancelled overwrite or an error occurred before the file was written). */ + private async createStandardMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + try { + console.debug('MDB | creating new note'); + + options.openNote ??= this.settings.openNoteInNewTab; + + if (this.settings.imageDownload) { + await this.downloadImageForMediaModel(mediaTypeModel); + } + + const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); + + options.folder ??= await this.mediaTypeManager.getFolder(mediaTypeModel, this.app); + + const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); + + if (this.settings.enableTemplaterIntegration) { + try { + await useTemplaterPluginInFile(this.app, targetFile); + } catch (e) { + console.warn(e); + new Notice(`${e}`); + } + } + return true; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + return false; + } + } + + private safeFileTreeSegment(title: string): string { + return replaceIllegalFileNameCharactersInString(title).replaceAll(/ +/g, ' ').trim(); + } + + private async ensureVaultFolder(folderPath: string): Promise { + const normalized = normalizePath(folderPath); + if (!(await this.app.vault.adapter.exists(normalized))) { + await this.app.vault.createFolder(normalized); + } + const folder = this.app.vault.getAbstractFileByPath(normalized); + if (!(folder instanceof TFolder)) { + throw new Error(`MDB | Expected folder at ${normalized}`); + } + return folder; + } + + private async importArtistDiscography(artist: ArtistModel, options: CreateNoteOptions): Promise { + try { + const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; + const genius = new GeniusClient(geniusToken); + if (!genius.isConfigured()) { + new Notice('Artist import: add a Genius API access token in settings to fetch lyrics.'); + } + + const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; + const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; + const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); + + const artistApi = this.apiManager.getApiByName('MusicBrainz Artist API') as MusicBrainzArtistAPI | undefined; + const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; + if (!artistApi || !musicBrainzApi) { + new Notice('MusicBrainz APIs not available.'); + return; + } + + const useTree = this.settings.artistUseFileTreeForSongs; + const childOptions: CreateNoteOptions = { + attachTemplate: true, + openNote: false, + attachFile: undefined, + folder: undefined, + }; + + const artistBaseFolder = await this.mediaTypeManager.getFolder(artist, this.app); + const artistNoteFolder = artistBaseFolder; + let albumNotesFolder = artistBaseFolder; + + if (useTree) { + const artistSeg = this.safeFileTreeSegment(artist.title); + const treeRootPath = normalizePath(`${artistBaseFolder.path}/${artistSeg}`); + albumNotesFolder = await this.ensureVaultFolder(treeRootPath); + } + + const artistNoteCreated = await this.createStandardMediaDbNoteFromModel(artist, { ...options, folder: artistNoteFolder }); + if (!artistNoteCreated) { + return; + } + + let releaseGroupIds: string[]; + try { + releaseGroupIds = await artistApi.listStudioAlbumReleaseGroupIds(artist.id); + } catch (e) { + new Notice(`Could not load albums: ${e}`); + return; + } + + new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${artist.title}…`); + + for (const rgId of releaseGroupIds) { + await new Promise(r => setTimeout(r, 1100)); + let release: MusicReleaseModel; + try { + const model = await musicBrainzApi.getById(rgId); + release = model as MusicReleaseModel; + } catch (e) { + console.warn(`MDB | Skipping release group ${rgId}:`, e); + continue; + } + + let songNotesFolder: TFolder | undefined; + if (useTree) { + const albumSeg = this.safeFileTreeSegment(release.title); + songNotesFolder = await this.ensureVaultFolder(normalizePath(`${albumNotesFolder.path}/${albumSeg}`)); + } + + const releaseOpts: CreateNoteOptions = useTree ? { ...childOptions, folder: albumNotesFolder } : { ...childOptions }; + + const albumNoteCreated = await this.createStandardMediaDbNoteFromModel(release, releaseOpts); + if (!albumNoteCreated) { + continue; + } + + for (const track of release.tracks) { + let lyrics = ''; + let geniusUrl = ''; + if (genius.isConfigured()) { + await new Promise(r => setTimeout(r, 500)); + const hit = await genius.searchFirstSongHit(`${artist.title} ${track.title}`); + if (hit) { + geniusUrl = hit.url; + await new Promise(r => setTimeout(r, 600)); + lyrics = await genius.fetchLyricsFromSongPage(hit.url); + } + } + + let spotifyUrl = ''; + if (track.recordingId) { + await new Promise(r => setTimeout(r, 1100)); + try { + spotifyUrl = await musicBrainzApi.fetchSpotifyUrlForRecording(track.recordingId); + } catch (e) { + console.warn(`MDB | Spotify URL for recording ${track.recordingId}:`, e); + } + } + if (!spotifyUrl && spotify.isConfigured()) { + const primaryArtist = release.artists[0] ?? artist.title; + console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); + try { + spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); + } catch (e) { + console.warn(`MDB | Spotify search for "${track.title}":`, e); + } + } + + const song = new SongModel({ + type: 'song', + title: track.title, + englishTitle: track.title, + year: release.year, + releaseDate: release.releaseDate, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: geniusUrl || release.url, + id: `${release.id}-t${track.number}`, + image: release.image, + subType: 'song', + genres: release.genres ?? [], + artists: release.artists.length > 0 ? release.artists : [artist.title], + albumTitle: release.title, + albumReleaseGroupId: release.id, + trackNumber: track.number, + duration: track.duration, + featuredArtists: track.featuredArtists, + geniusUrl, + spotifyUrl, + lyrics, + userData: { personalRating: 0 }, + }); + + const songOpts: CreateNoteOptions = useTree && songNotesFolder ? { ...childOptions, folder: songNotesFolder } : { ...childOptions }; + + await this.createStandardMediaDbNoteFromModel(song, songOpts); + } + } + + new Notice(`Finished artist import for ${artist.title}.`); + } catch (e) { + console.warn(e); + new Notice(`${e}`); + } + } + + /** + * Tries to download the image for a media model. + * + * @param mediaTypeModel + * @returns true if the image was downloaded, false otherwise + */ + private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise { + if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { + try { + const imageUrl = mediaTypeModel.image; + const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; + const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; + const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`); + + if (!this.app.vault.getAbstractFileByPath(this.settings.imageFolder)) { + await this.app.vault.createFolder(this.settings.imageFolder); + } + + if (!this.app.vault.getAbstractFileByPath(imagePath)) { + const response = await requestUrl({ url: imageUrl, method: 'GET' }); + await this.app.vault.createBinary(imagePath, response.arrayBuffer); + } + + // Update model to use local image path + mediaTypeModel.image = `[[${imagePath}]]`; + return true; + } catch (e) { + console.warn('MDB | Failed to download image:', e); + } + } + + return false; + } + + async downloadImagesInFolder(folder: TFolder): Promise { + new Notice(`MDB | Scanning for images to download in ${folder.name}...`); + const files = folder.children.filter((c): c is TFile => c instanceof TFile && c.extension === 'md'); + const startTime = Date.now(); + let downloaded = 0; + let failed = 0; + const erroredFiles: { filePath: string, error: string }[] = []; + + for (const file of files) { + const result = await this.downloadImagesInFile(file, true); + if (result.success) { + downloaded++; + } else if (!result.skipped) { + failed++; + if (result.error) erroredFiles.push({ filePath: file.path, error: result.error }); + } + // wait slightly as anti-rate limit + if (!result.skipped) { + await new Promise(r => setTimeout(r, 600)); + } + } + + if (failed > 0 && erroredFiles.length > 0) { + const title = `MDB - image download error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.app.vault.create(filePath, fileContent); + } + + new CompletionModal(this.app, { + title: 'Image Download Complete', + icon: '🖼️', + total: downloaded + failed, + success: downloaded, + errors: failed, + elapsedMs: Date.now() - startTime, + notes: failed > 0 ? ['Some images could not be downloaded. A detailed report file has been created in your vault folder.'] : [], + }).open(); + } + + /** + * Downloads images for a single file. + * @returns object detailing success, possible errors, or whether it was skipped + */ + async downloadImagesInFile(file: TFile, silent: boolean = false): Promise<{ success: boolean; skipped?: boolean; error?: string }> { + const metadata = this.getMetadataFromFileCache(file); + if (typeof metadata.image === 'string' && metadata.image.startsWith('http')) { + try { + const imageUrl = metadata.image; + const extMatch = imageUrl.split('?')[0].match(/\.([a-zA-Z0-9]+)$/); + const ext = extMatch ? extMatch[1] : 'jpg'; + const imgName = replaceIllegalFileNameCharactersInString(file.basename) + '.' + ext; + const imgFolder = await this.ensureVaultFolder(this.settings.imageFolder); + const imagePath = `${imgFolder.path}/${imgName}`; + + if (!this.app.vault.getAbstractFileByPath(imagePath)) { + const response = await requestUrl({ url: imageUrl, method: 'GET' }); + await this.app.vault.createBinary(imagePath, response.arrayBuffer); + } + + await this.app.fileManager.processFrontMatter(file, (frontmatter: any) => { + frontmatter.image = `[[${imagePath}]]`; + }); + if (!silent) new Notice(`MDB | Image downloaded for ${file.basename}`); + return { success: true }; + } catch (e) { + console.error("MDB | Image download failed for", file.path, e); + if (!silent) new Notice(`MDB | Image download failed for ${file.basename}`); + return { success: false, error: `${e}` }; + } + } + if (!silent) new Notice(`MDB | No external image found in ${file.basename}`); + return { success: false, skipped: true }; + } + + private metadataRecordForNewNote(mediaTypeModel: MediaTypeModel): Record { + let meta: Record; + if (this.settings.useDefaultFrontMatter) { + meta = mediaTypeModel.toMetaDataObject(); + } else { + meta = { + id: mediaTypeModel.id, + type: mediaTypeModel.type, + dataSource: mediaTypeModel.dataSource, + }; + } + return meta; + } + + generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { + mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); + const fileMetadata = this.modelPropertyMapper.convertObject(this.metadataRecordForNewNote(mediaTypeModel)); + return stringifyYaml(fileMetadata); + } + + /** + * Generates the content of a note from a media model and some options. + * + * @param mediaTypeModel + * @param options + */ + async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { + mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); + + let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); + let fileMetadata: Record = this.modelPropertyMapper.convertObject( + this.metadataRecordForNewNote(mediaTypeModel), + ); + + let fileContent = ''; + template = options.attachTemplate ? template : ''; + + ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); + ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); + + // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- + const entityWikiProps = this.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + if (entityWikiProps.length > 0) { + const folderPrefix = this.settings.wikiFolder ? `${this.settings.wikiFolder}/` : ''; + const isEnabled = this.settings.enableWikiLinkParsing; + const formatWiki = (v: unknown) => { + if (typeof v !== 'string') return v; + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + return isEnabled ? `[[${folderPrefix}${clean}|${clean}]]` : clean.trim(); + }; + + for (const [key, value] of Object.entries(fileMetadata)) { + if (key === 'aliases') continue; + if (entityWikiProps.includes(key.toLowerCase())) { + if (typeof value === 'string') { + fileMetadata[key] = formatWiki(value); + } else if (Array.isArray(value)) { + fileMetadata[key] = value.map(formatWiki); + } + } + } + } + + // --- Auto-Tag Logic --- + const tagProps = this.settings.autoTagProperties.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + if (this.settings.enableAutoTagging && tagProps.length > 0) { + const existingTags: string[] = Array.isArray(fileMetadata['tags']) ? (fileMetadata['tags'] as string[]) : []; + const newTags = new Set(existingTags.filter(t => typeof t === 'string' && t.trim() !== '')); + + for (const [key, value] of Object.entries(fileMetadata)) { + if (tagProps.includes(key.toLowerCase()) && value) { + const valuesToTag = Array.isArray(value) ? value : [value]; + for (let v of valuesToTag) { + if (typeof v === 'string') { + v = String(v).replace(/^\[\[(.*?)\]\]$/, '$1'); + if (v.includes('|')) { + v = v.split('|')[1]; + } + const sanitized = v + .trim() + .replace(/\s+/g, '-') + .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') + .toLowerCase(); + + if (sanitized) newTags.add(sanitized); + } + } + } + } + + if (newTags.size > 0) { + fileMetadata['tags'] = Array.from(newTags); + } + } + + if (mediaTypeModel.getMediaType() === MediaType.Song) { + const song = mediaTypeModel as SongModel; + if(song.lyrics.length > 0) { + fileContent += `# Lyrics\n\`\`\`\n${song.lyrics}\n\`\`\`\n`; + } + } + + if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { + // Include the media variable in all templater commands by using a top level JavaScript execution command. + const mediaJson = JSON.stringify(mediaTypeModel, (key, value: unknown) => (key === 'lyrics' ? undefined : value)); + fileContent = `---\n<%* const media = ${mediaJson} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } else { + fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; + } + + return fileContent; + } + + extractManualTags(metadata: Record, autoTagKeys: string): string[] { + const allTagsRaw = metadata['tags']; + const allTags = Array.isArray(allTagsRaw) ? allTagsRaw : typeof allTagsRaw === 'string' ? [allTagsRaw] : []; + if (allTags.length === 0) return []; + + const tagProps = autoTagKeys.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + const autoTagValues = new Set(); + + for (const [key, value] of Object.entries(metadata)) { + if (tagProps.includes(key.toLowerCase()) && value) { + const valuesToTag = Array.isArray(value) ? value : [value]; + for (const v of valuesToTag) { + if (typeof v === 'string') { + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + const sanitized = clean.trim().replace(/\s+/g, '-').replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '').toLowerCase(); + if (sanitized) autoTagValues.add(sanitized); + } + } + } + } + + return allTags.map(t => String(t).trim()).filter(t => t && !autoTagValues.has(t.toLowerCase()) && !t.toLowerCase().startsWith('mediadb/')); + } + + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!fileToAttach) { + return { fileMetadata: fileMetadata, fileContent: fileContent }; + } + + const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); + + // Rescue arrays that Object.assign would normally crush + const rescueArray = (key: string) => { + const arr = attachFileMetadata[key]; + if (Array.isArray(arr)) return [...arr as string[]]; + if (typeof arr === 'string' && arr.trim()) return [arr]; + return []; + }; + const oldManualTags = this.extractManualTags(attachFileMetadata, this.settings.autoTagProperties); + const oldAliases = rescueArray('aliases'); + + // TODO: better object merging + fileMetadata = Object.assign(attachFileMetadata, fileMetadata); + + // Merge tags cleanly (Preserving only manual user tags, discarding old ghost auto-tags!) + const newObjTags = fileMetadata['tags']; + const finalTags = new Set([...oldManualTags, ...(Array.isArray(newObjTags) ? newObjTags : typeof newObjTags === 'string' ? [newObjTags] : [])].map(t => String(t).trim())); + if (finalTags.size > 0) fileMetadata['tags'] = Array.from(finalTags); + + // Merge aliases cleanly + const newObjAliases = fileMetadata['aliases']; + const finalAliases = new Set([...oldAliases, ...(Array.isArray(newObjAliases) ? newObjAliases : typeof newObjAliases === 'string' ? [newObjAliases] : [])].map(a => String(a).trim())); + if (finalAliases.size > 0) fileMetadata['aliases'] = Array.from(finalAliases); + + let attachFileContent: string = await this.app.vault.read(fileToAttach); + const regExp = new RegExp(this.frontMatterRexExpPattern); + attachFileContent = attachFileContent.replace(regExp, ''); + attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; + fileContent += attachFileContent; + + return { fileMetadata: fileMetadata, fileContent: fileContent }; + } + + async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { + if (!template) { + return { fileMetadata: fileMetadata, fileContent: fileContent }; + } + + const templateMetadata = this.getMetaDataFromFileContent(template); + // TODO: better object merging + fileMetadata = Object.assign(templateMetadata, fileMetadata); + + const regExp = new RegExp(this.frontMatterRexExpPattern); + const attachFileContent = template.replace(regExp, ''); + fileContent += attachFileContent; + + return { fileMetadata: fileMetadata, fileContent: fileContent }; + } + + getMetaDataFromFileContent(fileContent: string): Metadata { + let metadata: Metadata; + + const regExp = new RegExp(this.frontMatterRexExpPattern); + const frontMatterRegExpResult = regExp.exec(fileContent); + if (!frontMatterRegExpResult) { + return {}; + } + let frontMatter = frontMatterRegExpResult[0]; + if (!frontMatter) { + return {}; + } + frontMatter = frontMatter.substring(4); + frontMatter = frontMatter.substring(0, frontMatter.length - 3); + + metadata = parseYaml(frontMatter) as Metadata; + + if (!metadata) { + metadata = {}; + } + + console.debug(`MDB | metadata read from file content`, metadata); + + return metadata; + } + + getMetadataFromFileCache(file: TFile): Metadata { + const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; + return structuredClone(metadata ?? {}); + } + + /** + * Creates a note in the vault. + * + * @param fileName + * @param fileContent + * @param options + */ + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { + // find and possibly create the folder set in settings or passed in folder + const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); + + if (!folder || !(folder instanceof TFolder)) { + throw new Error('MDB | invalid folder'); + } + + fileName = replaceIllegalFileNameCharactersInString(fileName); + const filePath = `${folder.path}/${fileName}.md`; + + // look if file already exists and ask if it should be overwritten + const file = this.app.vault.getAbstractFileByPath(filePath); + if (file) { + let shouldOverwrite = options.overwrite; + if (!shouldOverwrite) { + shouldOverwrite = await new Promise(resolve => { + new ConfirmOverwriteModal(this.app, fileName, resolve).open(); + }); + } + + if (!shouldOverwrite) { + throw new Error('MDB | file creation cancelled by user'); + } + + await this.app.vault.delete(file); + } + + // create the file + const targetFile = await this.app.vault.create(filePath, fileContent); + console.debug(`MDB | created new file at ${filePath}`); + + // open newly created file + if (options.openNote) { + const activeLeaf = this.app.workspace.getUnpinnedLeaf(); + if (!activeLeaf) { + console.warn('MDB | no active leaf, not opening newly created note'); + return targetFile; + } + await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); + } + + return targetFile; + } + + // --- AutoTracker Ribbon Logic --- + public _ribbonEl: HTMLElement | null = null; + refreshAutoTrackerRibbon() { + if (!this._ribbonEl) { + this._ribbonEl = this.addRibbonIcon('sync', 'Media DB: Auto-Tracker Sync', () => { + if (this.autoTrackerHelper.isScanning) { + new Notice("Auto-Tracker is currently syncing in the background."); + } else { + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate); + }).open(); + } + }); + this._ribbonEl.addClass('obsidian-media-db-plugin-ribbon-class'); + } + + if (this.autoTrackerHelper.isScanning) { + this._ribbonEl.addClass('media-db-spin-animation'); + } else { + this._ribbonEl.removeClass('media-db-spin-animation'); + } + } + + /** + * Update the active note by querying the API again. + * Tries to read the type and id of the active note (and dataSource when required). If successful it will query the api, delete the old note and create a new one. + */ + async updateActiveNote(onlyMetadata: boolean = false): Promise { + const activeFile = this.app.workspace.getActiveFile() ?? undefined; + if (!activeFile) { + throw new Error('MDB | there is no active note'); + } + return this.updateNote(activeFile, onlyMetadata, true, false); + } + + async updateNote(activeFile: TFile, onlyMetadata: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { + + let metadata = this.getMetadataFromFileCache(activeFile); + metadata = this.modelPropertyMapper.convertObjectBack(metadata); + + console.debug(`MDB | read metadata`, metadata); + + if (!metadata?.type || !metadata?.id) { + throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); + } + + const mediaType = resolveMetadataTypeToMediaType(this.settings, metadata.type); + if (mediaType === undefined) { + throw new Error('MDB | active note type is not recognized; check Settings → Note type for each media kind'); + } + let dataSource = typeof metadata.dataSource === 'string' ? metadata.dataSource.trim() : ''; + if ( + !dataSource && + musicBrainzRegisteredApiName(mediaType) + ) { + dataSource = MUSICBRAINZ_NOTE_DATA_SOURCE; + } + if (!dataSource) { + throw new Error('MDB | active note is missing dataSource (required for this media type)'); + } + + const validOldMetadata: MediaTypeModelObj = { ...metadata, dataSource } as unknown as MediaTypeModelObj; + console.debug(`MDB | validOldMetadata`, validOldMetadata); + + const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, mediaType); + console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); + + let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource, mediaType); + if (!newMediaTypeModel) { + return; + } + + newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); + console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); + + if (onlyMetadata) { + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); + } else { + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); + } + } + + async loadSettings(): Promise { + const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; + const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); + const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); + + // Migrate property mappings using the dedicated migration method + const migratedModels = PropertyMappingModel.migrateModels( + loadedSettings.propertyMappingModels || [], + defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), + ); + + // Store as plain data for serialization (canonical order matches settings UI) + loadedSettings.propertyMappingModels = propertyMappingModelsInDisplayOrder(migratedModels.map(m => m.toJSON())); + + // --- MIGRATION: Band to Artist --- + const anyLoaded = diskSettings as any; + if (anyLoaded) { + if (anyLoaded.bandTemplate && !loadedSettings.artistTemplate) loadedSettings.artistTemplate = anyLoaded.bandTemplate; + if (anyLoaded.bandFolder && !loadedSettings.artistFolder) loadedSettings.artistFolder = anyLoaded.bandFolder; + if (anyLoaded.bandFileNameTemplate && !loadedSettings.artistFileNameTemplate) loadedSettings.artistFileNameTemplate = anyLoaded.bandFileNameTemplate; + if (anyLoaded.bandNoteType && !loadedSettings.artistNoteType) loadedSettings.artistNoteType = anyLoaded.bandNoteType; + if (anyLoaded.bandUseFileTreeForSongs !== undefined && loadedSettings.artistUseFileTreeForSongs === false) loadedSettings.artistUseFileTreeForSongs = anyLoaded.bandUseFileTreeForSongs; + if (anyLoaded.MusicBrainzBandAPI_disabledMediaTypes && !loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes) loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes = anyLoaded.MusicBrainzBandAPI_disabledMediaTypes; + } + + this.settings = loadedSettings; + } + + async saveSettings(): Promise { + this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); + this.dateFormatter.setFormat(this.settings.customDateFormat); + + await this.saveData(this.settings); + } +} diff --git a/modals/BulkUpdateConfirmModal.ts b/modals/BulkUpdateConfirmModal.ts new file mode 100644 index 00000000..5b9effe4 --- /dev/null +++ b/modals/BulkUpdateConfirmModal.ts @@ -0,0 +1,39 @@ +import { type App, Modal, Setting } from 'obsidian'; + +export class BulkUpdateConfirmModal extends Modal { + onSubmit: (silent: boolean) => void; + silentUpdate: boolean = true; + + constructor(app: App, onSubmit: (silent: boolean) => void) { + super(app); + this.onSubmit = onSubmit; + } + + onOpen() { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'Bulk Update Metadata' }); + contentEl.createEl('p', { text: 'You are about to scan and update metadata for notes in this folder.' }); + + new Setting(contentEl) + .setName('Update Silently (No Confirmations)') + .setDesc('If enabled, all updates will aggressively overwrite the note frontmatter without asking for individual confirmation for each file.') + .addToggle(toggle => toggle + .setValue(this.silentUpdate) + .onChange(value => (this.silentUpdate = value)) + ); + + new Setting(contentEl) + .addButton(btn => btn + .setButtonText('Start Update') + .setCta() + .onClick(() => { + this.close(); + this.onSubmit(this.silentUpdate); + })); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/modals/CompletionModal.ts b/modals/CompletionModal.ts new file mode 100644 index 00000000..233c3008 --- /dev/null +++ b/modals/CompletionModal.ts @@ -0,0 +1,85 @@ +import { type App, Modal, ButtonComponent } from 'obsidian'; + +export interface CompletionResult { + /** Title shown in the modal header */ + title: string; + /** Icon emoji for the operation type */ + icon?: string; + /** Total number of items processed */ + total: number; + /** Number of successfully processed items */ + success: number; + /** Number of failed items */ + errors: number; + /** Number of skipped items (optional) */ + skipped?: number; + /** Elapsed time in milliseconds */ + elapsedMs?: number; + /** Optional extra lines shown below the stats */ + notes?: string[]; +} + +export class CompletionModal extends Modal { + private result: CompletionResult; + + constructor(app: App, result: CompletionResult) { + super(app); + this.result = result; + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('mdb-completion-modal'); + + const r = this.result; + const icon = r.icon ?? '✅'; + const allSuccess = r.errors === 0; + + // Header + const header = contentEl.createEl('div', { cls: 'mdb-completion-header' }); + header.createEl('span', { cls: 'mdb-completion-icon', text: allSuccess ? icon : '⚠️' }); + header.createEl('h2', { cls: 'mdb-completion-title', text: r.title }); + + // Stats + const stats = contentEl.createEl('div', { cls: 'mdb-completion-stats' }); + + this.addStatRow(stats, '📄 Total', `${r.total}`); + this.addStatRow(stats, '✅ Successful', `${r.success}`, 'success'); + this.addStatRow(stats, '❌ Errors', `${r.errors}`, r.errors > 0 ? 'error' : undefined); + + if (r.skipped !== undefined) { + this.addStatRow(stats, '⏭️ Skipped', `${r.skipped}`, 'skipped'); + } + + if (r.elapsedMs !== undefined) { + const secs = (r.elapsedMs / 1000).toFixed(1); + this.addStatRow(stats, '⏱️ Duration', `${secs}s`); + } + + // Notes + if (r.notes && r.notes.length > 0) { + const notesEl = contentEl.createEl('div', { cls: 'mdb-completion-notes' }); + for (const note of r.notes) { + notesEl.createEl('p', { text: note }); + } + } + + // Close button + const footer = contentEl.createEl('div', { cls: 'mdb-completion-footer' }); + new ButtonComponent(footer) + .setButtonText('Close') + .setCta() + .onClick(() => this.close()); + } + + onClose(): void { + this.contentEl.empty(); + } + + private addStatRow(container: HTMLElement, label: string, value: string, cls?: string): void { + const row = container.createEl('div', { cls: 'mdb-completion-row' }); + row.createEl('span', { cls: 'mdb-completion-label', text: label }); + row.createEl('span', { cls: `mdb-completion-value${cls ? ' mdb-stat-' + cls : ''}`, text: value }); + } +} diff --git a/modals/ConfirmOverwriteModal.ts b/modals/ConfirmOverwriteModal.ts new file mode 100644 index 00000000..9852da26 --- /dev/null +++ b/modals/ConfirmOverwriteModal.ts @@ -0,0 +1,45 @@ +import type { App } from 'obsidian'; +import { Modal, Setting } from 'obsidian'; + +export class ConfirmOverwriteModal extends Modal { + result: boolean = false; + onSubmit: (result: boolean) => void; + fileName: string; + + constructor(app: App, fileName: string, onSubmit: (result: boolean) => void) { + super(app); + this.fileName = fileName; + this.onSubmit = onSubmit; + } + + onOpen(): void { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'File already exists' }); + contentEl.createEl('p', { text: `The file "${this.fileName}" already exists. Do you want to overwrite it?` }); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + const bottomSettingRow = new Setting(contentEl); + + bottomSettingRow.addButton(btn => { + btn.setButtonText('No'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Yes'); + btn.setCta(); + btn.onClick(() => { + this.result = true; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + this.onSubmit(this.result); + } +} diff --git a/modals/MediaDbAdvancedSearchModal.ts b/modals/MediaDbAdvancedSearchModal.ts new file mode 100644 index 00000000..e0e80207 --- /dev/null +++ b/modals/MediaDbAdvancedSearchModal.ts @@ -0,0 +1,133 @@ +import type { ButtonComponent } from 'obsidian'; +import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; +import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; + +export class MediaDbAdvancedSearchModal extends Modal { + plugin: MediaDbPlugin; + + query: string; + isBusy: boolean; + title: string; + selectedApis: string[]; + + searchBtn?: ButtonComponent; + + submitCallback?: (res: AdvancedSearchModalData) => void; + closeCallback?: (err?: Error) => void; + + constructor(plugin: MediaDbPlugin, advancedSearchModalOptions: AdvancedSearchModalOptions) { + advancedSearchModalOptions = Object.assign({}, ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, advancedSearchModalOptions); + super(plugin.app); + + this.plugin = plugin; + this.selectedApis = []; + this.title = advancedSearchModalOptions.modalTitle ?? ''; + this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; + this.isBusy = false; + } + + setSubmitCb(submitCallback: (res: AdvancedSearchModalData) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCb(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + keyPressCallback(event: KeyboardEvent): void { + if (event.key === 'Enter') { + void this.search(); + } + } + + async search(): Promise { + if (!this.query || this.query.length < 3) { + new Notice('MDB | Query too short'); + return; + } + + const apis: string[] = this.selectedApis; + + if (apis.length === 0) { + new Notice('MDB | No API selected'); + return; + } + + if (!this.isBusy) { + this.isBusy = true; + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); + + this.submitCallback?.({ query: this.query, apis: apis }); + } + } + + onOpen(): void { + const { contentEl } = this; + + contentEl.createEl('h2', { text: this.title }); + + const placeholder = 'Search by title'; + const searchComponent = new TextComponent(contentEl); + searchComponent.inputEl.style.width = '100%'; + searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); + searchComponent.onChange(value => (this.query = value)); + searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); + + contentEl.appendChild(searchComponent.inputEl); + searchComponent.inputEl.focus(); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + contentEl.createEl('h3', { text: 'APIs to search' }); + + // const apiToggleComponents: Component[] = []; + for (const api of this.plugin.apiManager.apis) { + const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + + const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' }); + apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' }); + + const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + + const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); + apiToggleComponent.setTooltip(api.apiName); + apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); + apiToggleComponent.onChange(value => { + if (value) { + this.selectedApis.push(api.apiName); + } else { + this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); + } + }); + apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); + } + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + new Setting(contentEl) + .addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + void this.search(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; + }); + } + + onClose(): void { + this.closeCallback?.(); + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/modals/MediaDbBulkImportModal.ts b/modals/MediaDbBulkImportModal.ts new file mode 100644 index 00000000..1aee8eeb --- /dev/null +++ b/modals/MediaDbBulkImportModal.ts @@ -0,0 +1,134 @@ +import type { ButtonComponent } from 'obsidian'; +import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type { APIModel } from 'src/api/APIModel'; +import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper'; +import type MediaDbPlugin from '../main'; + +export class MediaDbBulkImportModal extends Modal { + plugin: MediaDbPlugin; + onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void; + selectedApi: string; + searchBtn?: ButtonComponent; + lookupMethod: BulkImportLookupMethod; + fieldName: string; + appendContent: boolean; + + constructor(plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void) { + super(plugin.app); + this.plugin = plugin; + this.onSubmit = onSubmit; + this.selectedApi = plugin.apiManager.apis[0].apiName; + this.lookupMethod = BulkImportLookupMethod.TITLE; + this.fieldName = ''; + this.appendContent = false; + } + + submit(): void { + this.onSubmit(this.selectedApi, this.lookupMethod, this.fieldName, this.appendContent); + this.close(); + } + + onOpen(): void { + const { contentEl } = this; + + contentEl.createEl('h2', { text: 'Import folder as Media DB entries' }); + + this.createDropdownEl( + contentEl, + 'API to search', + (value: string) => { + this.selectedApi = value; + }, + this.plugin.apiManager.apis.map((api: APIModel) => { + return { value: api.apiName, display: api.apiName }; + }), + ); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' }); + + const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + appendContentToggleTextWrapper.createEl('span', { + text: 'If this is enabled, the plugin will override metadata fields with the same name.', + cls: 'media-db-plugin-list-text', + }); + + const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + + const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper); + appendContentToggle.setValue(false); + appendContentToggle.onChange(value => (this.appendContent = value)); + appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + contentEl.createEl('h3', { text: 'Media lookup method' }); + contentEl.createEl('p', { + text: 'Choose whether to search the API by title (can return multiple results) or lookup directly using an ID (returns at most one result), and specify the name of the frontmatter property which contains the title or ID of the media.', + }); + + this.createDropdownEl( + contentEl, + 'Lookup media by', + (value: string) => { + this.lookupMethod = value as BulkImportLookupMethod; + }, + [ + { value: BulkImportLookupMethod.TITLE, display: 'Title' }, + { value: BulkImportLookupMethod.ID, display: 'ID' }, + ], + ); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' }); + + const fieldNameComponent = new TextComponent(fieldNameWrapperEl); + fieldNameComponent.setPlaceholder('title / id'); + fieldNameComponent.onChange(value => (this.fieldName = value)); + fieldNameComponent.inputEl.addEventListener('keydown', ke => { + if (ke.key === 'Enter') { + this.submit(); + } + }); + contentEl.appendChild(fieldNameWrapperEl); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + new Setting(contentEl) + .addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + this.submit(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; + }); + } + + createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void { + const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' }); + + const dropDownComponent = new DropdownComponent(wrapperEl); + dropDownComponent.onChange(onChange); + for (const option of options) { + dropDownComponent.addOption(option.value, option.display); + } + wrapperEl.appendChild(dropDownComponent.selectEl); + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/modals/MediaDbIdSearchModal.ts b/modals/MediaDbIdSearchModal.ts new file mode 100644 index 00000000..7027bb52 --- /dev/null +++ b/modals/MediaDbIdSearchModal.ts @@ -0,0 +1,119 @@ +import type { ButtonComponent } from 'obsidian'; +import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; +import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; + +export class MediaDbIdSearchModal extends Modal { + plugin: MediaDbPlugin; + + query: string; + isBusy: boolean; + title: string; + selectedApi: string; + + searchBtn?: ButtonComponent; + + submitCallback?: (res: IdSearchModalData, err?: Error) => void; + closeCallback?: (err?: Error) => void; + + constructor(plugin: MediaDbPlugin, idSearchModalOptions: IdSearchModalOptions) { + idSearchModalOptions = Object.assign({}, ID_SEARCH_MODAL_DEFAULT_OPTIONS, idSearchModalOptions); + super(plugin.app); + + this.plugin = plugin; + this.title = idSearchModalOptions.modalTitle ?? ''; + this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; + this.query = ''; + this.isBusy = false; + } + + setSubmitCb(submitCallback: (res: IdSearchModalData, err?: Error) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCb(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + keyPressCallback(event: KeyboardEvent): void { + if (event.key === 'Enter') { + void this.search(); + } + } + + async search(): Promise { + if (!this.query) { + new Notice('MDB | no Id entered'); + return; + } + + if (!this.selectedApi) { + new Notice('MDB | No API selected'); + return; + } + + if (!this.isBusy) { + this.isBusy = true; + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); + + this.submitCallback?.({ query: this.query, api: this.selectedApi }); + } + } + + onOpen(): void { + const { contentEl } = this; + + contentEl.createEl('h2', { text: this.title }); + + const placeholder = 'Search by id'; + const searchComponent = new TextComponent(contentEl); + searchComponent.inputEl.style.width = '100%'; + searchComponent.setPlaceholder(placeholder); + searchComponent.onChange(value => (this.query = value)); + searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); + + contentEl.appendChild(searchComponent.inputEl); + searchComponent.inputEl.focus(); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' }); + + const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper); + apiSelectorComponent.onChange((value: string) => { + this.selectedApi = value; + }); + for (const api of this.plugin.apiManager.apis) { + apiSelectorComponent.addOption(api.apiName, api.apiName); + } + apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + new Setting(contentEl) + .addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + void this.search(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; + }); + } + + onClose(): void { + this.closeCallback?.(); + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/modals/MediaDbPreviewModal.ts b/modals/MediaDbPreviewModal.ts new file mode 100644 index 00000000..931556c1 --- /dev/null +++ b/modals/MediaDbPreviewModal.ts @@ -0,0 +1,86 @@ +import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import type { MediaTypeModel } from 'src/models/MediaTypeModel'; +import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; +import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; + +export class MediaDbPreviewModal extends Modal { + plugin: MediaDbPlugin; + + elements: MediaTypeModel[]; + title: string; + markdownComponent: Component; + + submitCallback?: (previewModalData: PreviewModalData) => void; + closeCallback?: (err?: Error) => void; + + constructor(plugin: MediaDbPlugin, previewModalOptions: PreviewModalOptions) { + previewModalOptions = Object.assign({}, PREVIEW_MODAL_DEFAULT_OPTIONS, previewModalOptions); + + super(plugin.app); + + this.plugin = plugin; + this.title = previewModalOptions.modalTitle ?? ''; + this.elements = previewModalOptions.elements ?? []; + + this.markdownComponent = new Component(); + } + + setSubmitCb(submitCallback: (previewModalData: PreviewModalData) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCb(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + async preview(): Promise { + const { contentEl } = this; + contentEl.addClass('media-db-plugin-preview-modal'); + + contentEl.createEl('h2', { text: this.title }); + + const previewWrapper = contentEl.createDiv({ cls: 'media-db-plugin-preview-wrapper' }); + + this.markdownComponent.load(); + + for (const result of this.elements) { + previewWrapper.createEl('h3', { text: result.englishTitle }); + const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' }); + + let fileContent = this.plugin.generateMediaDbNoteFrontmatterPreview(result); + fileContent = `\`\`\`yaml\n${fileContent}\`\`\``; + + try { + // TODO: fix this not rendering the frontmatter any more + await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent); + } catch (e) { + console.warn(`mdb | error during rendering of preview`, e); + } + } + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + const bottomSettingRow = new Setting(contentEl); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => this.submitCallback?.({ confirmed: true })); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + } + + onOpen(): void { + void this.preview(); + } + + onClose(): void { + this.markdownComponent.unload(); + this.closeCallback?.(); + } +} diff --git a/modals/MediaDbSearchModal.ts b/modals/MediaDbSearchModal.ts new file mode 100644 index 00000000..8332f5ab --- /dev/null +++ b/modals/MediaDbSearchModal.ts @@ -0,0 +1,145 @@ +import type { ButtonComponent } from 'obsidian'; +import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; +import type MediaDbPlugin from '../main'; +import type { MediaType } from '../utils/MediaType'; +import { MEDIA_TYPES } from '../utils/MediaTypeManager'; +import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; +import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; +import { mediaTypeDisplayName } from '../utils/Utils'; + +export class MediaDbSearchModal extends Modal { + plugin: MediaDbPlugin; + + query: string; + isBusy: boolean; + title: string; + selectedTypes: MediaType[]; + + searchBtn?: ButtonComponent; + + submitCallback?: (res: SearchModalData) => void; + closeCallback?: (err?: Error) => void; + + constructor(plugin: MediaDbPlugin, searchModalOptions: SearchModalOptions) { + searchModalOptions = Object.assign({}, SEARCH_MODAL_DEFAULT_OPTIONS, searchModalOptions); + super(plugin.app); + + this.plugin = plugin; + this.selectedTypes = [...(searchModalOptions.preselectedTypes ?? [])]; + this.title = searchModalOptions.modalTitle ?? ''; + this.query = searchModalOptions.prefilledSearchString ?? ''; + this.isBusy = false; + } + + setSubmitCb(submitCallback: (res: SearchModalData) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCb(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + keyPressCallback(event: KeyboardEvent): void { + if (event.key === 'Enter') { + void this.search(); + } + } + + async search(): Promise { + if (!this.query || this.query.length < 3) { + new Notice('MDB | Query too short'); + return; + } + + const types: MediaType[] = this.selectedTypes; + + if (types.length === 0) { + new Notice('MDB | No Type selected'); + return; + } + + if (!this.isBusy) { + this.isBusy = true; + this.searchBtn?.setDisabled(false); + this.searchBtn?.setButtonText('Searching...'); + + this.submitCallback?.({ query: this.query, types: types }); + } + } + + onOpen(): void { + const { contentEl } = this; + + contentEl.createEl('h2', { text: this.title }); + + const placeholder = 'Search by title'; + const searchComponent = new TextComponent(contentEl); + let currentToggle: ToggleComponent | undefined = undefined; + + searchComponent.inputEl.style.width = '100%'; + searchComponent.setPlaceholder(placeholder); + searchComponent.setValue(this.query); + searchComponent.onChange(value => (this.query = value)); + searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); + + contentEl.appendChild(searchComponent.inputEl); + searchComponent.inputEl.focus(); + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + contentEl.createEl('h3', { text: 'APIs to search' }); + + for (const mediaType of MEDIA_TYPES) { + const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); + + const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); + apiToggleTextWrapper.createEl('span', { text: mediaTypeDisplayName(mediaType), cls: 'media-db-plugin-list-text' }); + + const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); + + const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); + apiToggleComponent.setTooltip(mediaTypeDisplayName(mediaType)); + apiToggleComponent.setValue(this.selectedTypes.contains(mediaType)); + if (apiToggleComponent.getValue()) { + currentToggle = apiToggleComponent; + } + apiToggleComponent.onChange(value => { + if (value) { + if (currentToggle && currentToggle !== apiToggleComponent) { + currentToggle.setValue(false); + this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); + } + currentToggle = apiToggleComponent; + this.selectedTypes.push(mediaType); + } else { + currentToggle = undefined; + this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); + } + }); + apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); + } + + contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); + + new Setting(contentEl) + .addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }) + .addButton(btn => { + btn.setButtonText('Ok'); + btn.setCta(); + btn.onClick(() => { + void this.search(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + this.searchBtn = btn; + }); + } + + onClose(): void { + this.closeCallback?.(); + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/modals/MediaDbSearchResultModal.ts b/modals/MediaDbSearchResultModal.ts new file mode 100644 index 00000000..786df40a --- /dev/null +++ b/modals/MediaDbSearchResultModal.ts @@ -0,0 +1,66 @@ +import type MediaDbPlugin from '../main'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; +import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; +import { SelectModal } from './SelectModal'; + +export class MediaDbSearchResultModal extends SelectModal { + plugin: MediaDbPlugin; + + busy: boolean; + sendCallback: boolean; + + submitCallback?: (res: SelectModalData) => void; + closeCallback?: (err?: Error) => void; + skipCallback?: () => void; + submitButtonText: string; + + constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { + selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions); + super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect); + this.plugin = plugin; + this.title = selectModalOptions.modalTitle ?? ''; + this.description = selectModalOptions.description ?? 'Select one or multiple search results.'; + this.addSkipButton = selectModalOptions.skipButton ?? false; + this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok'; + this.busy = false; + this.sendCallback = false; + } + + setSubmitCb(submitCallback: (res: SelectModalData) => void): void { + this.submitCallback = submitCallback; + } + + setCloseCb(closeCallback: (err?: Error) => void): void { + this.closeCallback = closeCallback; + } + + setSkipCallback(skipCallback: () => void): void { + this.skipCallback = skipCallback; + } + + // Renders each suggestion item. + renderElement(item: MediaTypeModel, el: HTMLElement): void { + el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); + el.createEl('small', { text: `${item.getSummary()}\n` }); + el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); + } + + // Perform action on the selected suggestion. + submit(): void { + if (!this.busy) { + this.busy = true; + this.submitButton?.setButtonText('Creating entry...'); + this.submitCallback?.({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) }); + } + } + + skip(): void { + this.skipButton?.setButtonText('Skipping...'); + this.skipCallback?.(); + } + + onClose(): void { + this.closeCallback?.(); + } +} diff --git a/modals/MediaDbSeasonSelectModal.ts b/modals/MediaDbSeasonSelectModal.ts new file mode 100644 index 00000000..0066c31e --- /dev/null +++ b/modals/MediaDbSeasonSelectModal.ts @@ -0,0 +1,50 @@ +import type MediaDbPlugin from '../main'; +import { SelectModal } from './SelectModal'; + +export interface SeasonSelectModalElement { + season_number: number; + name: string; + air_date?: string; + poster_path?: string; +} + +export class MediaDbSeasonSelectModal extends SelectModal { + plugin: MediaDbPlugin; + submitCallback?: (selectedSeasons: SeasonSelectModalElement[]) => void; + closeCallback?: (err?: Error) => void; + seriesName?: string; + + constructor(plugin: MediaDbPlugin, seasons: SeasonSelectModalElement[], multiSelect = true, seriesName?: string) { + super(plugin.app, seasons, multiSelect); + this.plugin = plugin; + this.seriesName = seriesName; + this.title = `Select seasons for${seriesName ? ` ${seriesName}` : ''}`; + this.description = 'Select one or more seasons to create notes for.'; + this.submitButtonText = 'Create Entry'; + } + + renderElement(season: SeasonSelectModalElement, el: HTMLElement): void { + el.createEl('div', { text: `${season.name}` }); + if (season.air_date) { + el.createEl('small', { text: `Air date: ${season.air_date}` }); + } + } + + submit(): void { + const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value); + this.submitCallback?.(selected); + this.close(); + } + + skip(): void { + this.close(); + } + + setSubmitCb(cb: (selectedSeasons: SeasonSelectModalElement[]) => void): void { + this.submitCallback = cb; + } + + setCloseCb(cb: (err?: Error) => void): void { + this.closeCallback = cb; + } +} diff --git a/modals/PropertyMappingModal.ts b/modals/PropertyMappingModal.ts new file mode 100644 index 00000000..8f5adccd --- /dev/null +++ b/modals/PropertyMappingModal.ts @@ -0,0 +1,59 @@ +import type { App } from 'obsidian'; +import { Modal } from 'obsidian'; +import { render } from 'solid-js/web'; +import type MediaDbPlugin from '../main'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; +import type { PropertyMappingModelData } from '../settings/PropertyMapping'; +import PropertyMappingModelComponent from '../settings/PropertyMappingModelComponent'; + +export class PropertyMappingModal extends Modal { + private disposeSolid?: () => void; + + constructor( + app: App, + private readonly plugin: MediaDbPlugin, + private readonly mediaType: MediaType, + ) { + super(app); + } + + onOpen(): void { + const { contentEl } = this; + this.setTitle(`Property mappings — ${mediaTypeDisplayName(this.mediaType)}`); + + const modelData = this.plugin.settings.propertyMappingModels.find(m => m.type === this.mediaType); + if (!modelData) { + contentEl.createEl('p', { text: 'No property mapping model found for this media type.' }); + return; + } + + contentEl.createEl('p', { + cls: 'mod-muted', + text: 'Choose whether each metadata field stays as-is, is renamed in front matter, or is omitted. Changes are saved automatically when valid.', + }); + + const root = contentEl.createDiv(); + this.disposeSolid = render( + () => + PropertyMappingModelComponent({ + model: structuredClone(modelData), + showMediaTypeTitle: false, + save: (model: PropertyMappingModelData): void => { + const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); + if (index !== -1) { + this.plugin.settings.propertyMappingModels[index] = model; + } + void this.plugin.saveSettings(); + }, + }), + root, + ); + } + + onClose(): void { + this.disposeSolid?.(); + this.disposeSolid = undefined; + this.contentEl.empty(); + } +} diff --git a/modals/SelectModal.ts b/modals/SelectModal.ts new file mode 100644 index 00000000..d54b5f99 --- /dev/null +++ b/modals/SelectModal.ts @@ -0,0 +1,176 @@ +import type { App, ButtonComponent } from 'obsidian'; +import { Modal, Setting } from 'obsidian'; +import { mod } from '../utils/Utils'; +import { SelectModalElement } from './SelectModalElement'; + +export abstract class SelectModal extends Modal { + allowMultiSelect: boolean; + + title: string; + description: string; + addSkipButton: boolean; + cancelButton?: ButtonComponent; + skipButton?: ButtonComponent; + submitButton?: ButtonComponent; + submitButtonText: string; + + elementWrapper?: HTMLDivElement; + + elements: T[]; + selectModalElements: SelectModalElement[]; + + protected constructor(app: App, elements: T[], allowMultiSelect: boolean = true) { + super(app); + this.allowMultiSelect = allowMultiSelect; + + this.title = ''; + this.description = ''; + this.addSkipButton = false; + this.submitButtonText = 'Ok'; + this.cancelButton = undefined; + this.skipButton = undefined; + this.submitButton = undefined; + + this.elementWrapper = undefined; + + this.elements = elements; + this.selectModalElements = []; + + this.scope.register([], 'ArrowUp', evt => { + this.highlightUp(); + evt.preventDefault(); + }); + this.scope.register([], 'ArrowDown', evt => { + this.highlightDown(); + evt.preventDefault(); + }); + this.scope.register([], 'ArrowRight', () => { + this.activateHighlighted(); + }); + this.scope.register([], ' ', evt => { + if (this.elementWrapper && this.elementWrapper === document.activeElement) { + this.activateHighlighted(); + evt.preventDefault(); + } + }); + this.scope.register([], 'Enter', () => this.submit()); + } + + abstract renderElement(value: T, el: HTMLElement): void; + + abstract submit(): void; + + abstract skip(): void; + + disableAllOtherElements(elementId: number): void { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.id !== elementId) { + selectModalElement.setActive(false); + } + } + } + + deHighlightAllOtherElements(elementId: number): void { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.id !== elementId) { + selectModalElement.setHighlighted(false); + } + } + } + + onOpen(): void { + const { contentEl, titleEl } = this; + + titleEl.createEl('h2', { text: this.title }); + contentEl.addClass('media-db-plugin-select-modal'); + contentEl.createEl('p', { text: this.description }); + + this.elementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-select-wrapper' }); + this.elementWrapper.tabIndex = 0; + + let i = 0; + for (const element of this.elements) { + const selectModalElement = new SelectModalElement(element, this.elementWrapper, i, this, false); + + this.selectModalElements.push(selectModalElement); + + this.renderElement(element, selectModalElement.element); + + i += 1; + } + + this.selectModalElements.first()?.element.scrollIntoView(); + + const bottomSettingRow = new Setting(contentEl); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Cancel'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.cancelButton = btn; + }); + if (this.addSkipButton) { + bottomSettingRow.addButton(btn => { + btn.setButtonText('Skip'); + btn.onClick(() => this.skip()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.skipButton = btn; + }); + } + bottomSettingRow.addButton(btn => { + btn.setButtonText(this.submitButtonText); + btn.setCta(); + btn.onClick(() => this.submit()); + btn.buttonEl.addClass('media-db-plugin-button'); + this.submitButton = btn; + }); + } + + activateHighlighted(): void { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.isHighlighted()) { + selectModalElement.setActive(!selectModalElement.isActive()); + if (!this.allowMultiSelect) { + this.disableAllOtherElements(selectModalElement.id); + } + } + } + } + + highlightUp(): void { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.isHighlighted()) { + this.getPreviousSelectModalElement(selectModalElement).setHighlighted(true); + return; + } + } + + // nothing is highlighted + this.selectModalElements.last()?.setHighlighted(true); + } + + highlightDown(): void { + for (const selectModalElement of this.selectModalElements) { + if (selectModalElement.isHighlighted()) { + this.getNextSelectModalElement(selectModalElement).setHighlighted(true); + return; + } + } + + // nothing is highlighted + this.selectModalElements.first()?.setHighlighted(true); + } + + private getNextSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { + let nextId = selectModalElement.id + 1; + nextId = mod(nextId, this.selectModalElements.length); + + return this.selectModalElements.find(x => x.id === nextId)!; + } + + private getPreviousSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { + let nextId = selectModalElement.id - 1; + nextId = mod(nextId, this.selectModalElements.length); + + return this.selectModalElements.find(x => x.id === nextId)!; + } +} diff --git a/modals/SelectModalElement.ts b/modals/SelectModalElement.ts new file mode 100644 index 00000000..70c09f17 --- /dev/null +++ b/modals/SelectModalElement.ts @@ -0,0 +1,88 @@ +import type { SelectModal } from './SelectModal'; + +export class SelectModalElement { + selectModal: SelectModal; + value: T; + readonly id: number; + element: HTMLDivElement; + cssClass: string; + activeClass: string; + hoverClass: string; + private active: boolean; + private highlighted: boolean; + + constructor(value: T, parentElement: HTMLElement, id: number, selectModal: SelectModal, active: boolean = false) { + this.value = value; + this.id = id; + this.active = active; + this.selectModal = selectModal; + + this.cssClass = 'media-db-plugin-select-element'; + this.activeClass = 'media-db-plugin-select-element-selected'; + this.hoverClass = 'media-db-plugin-select-element-hover'; + + this.element = parentElement.createDiv({ cls: this.cssClass }); + this.element.id = this.getHTMLId(); + this.element.on('click', '#' + this.getHTMLId(), () => { + this.setActive(!this.active); + if (!this.selectModal.allowMultiSelect) { + this.selectModal.disableAllOtherElements(this.id); + } + }); + this.element.on('mouseenter', '#' + this.getHTMLId(), () => { + this.setHighlighted(true); + }); + this.element.on('mouseleave', '#' + this.getHTMLId(), () => { + this.setHighlighted(false); + }); + + this.highlighted = false; + } + + getHTMLId(): string { + return `media-db-plugin-select-element-${this.id}`; + } + + isHighlighted(): boolean { + return this.highlighted; + } + + setHighlighted(value: boolean): void { + this.highlighted = value; + if (this.highlighted) { + this.addClass(this.hoverClass); + this.selectModal.deHighlightAllOtherElements(this.id); + } else { + this.removeClass(this.hoverClass); + } + } + + isActive(): boolean { + return this.active; + } + + setActive(active: boolean): void { + this.active = active; + this.update(); + } + + update(): void { + if (this.active) { + this.addClass(this.activeClass); + } else { + this.removeClass(this.activeClass); + } + } + + addClass(cssClass: string): void { + if (!this.element.hasClass(cssClass)) { + this.element.addClass(cssClass); + } + } + + removeClass(cssClass: string): void { + if (this.element.hasClass(cssClass)) { + this.element.removeClass(cssClass); + } + } +} diff --git a/models/ArtistModel.ts b/models/ArtistModel.ts new file mode 100644 index 00000000..9683da38 --- /dev/null +++ b/models/ArtistModel.ts @@ -0,0 +1,63 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type ArtistData = ModelToData; + +export class ArtistModel extends MediaTypeModel { + genres: string[]; + country: string; + image: string; + officialWebsite: string; + disambiguation: string; + /** ISNI(s) from the data source; comma-separated if multiple. */ + isni: string; + beginYear: string; + releaseDate: string; + + userData: { + personalRating: number; + }; + + constructor(obj: ArtistData) { + super(); + + this.genres = []; + this.country = ''; + this.image = ''; + this.officialWebsite = ''; + this.disambiguation = ''; + this.isni = ''; + this.beginYear = ''; + this.releaseDate = ''; + + this.userData = { + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + this.releaseDate = obj.releaseDate ?? ''; + } + + getTags(): string[] { + return [mediaDbTag, 'music', 'artist']; + } + + getMediaType(): MediaType { + return MediaType.Artist; + } + + getSummary(): string { + let summary = this.title; + if (this.beginYear) summary += ` (formed ${this.beginYear})`; + if (this.disambiguation) summary += ` — ${this.disambiguation}`; + return summary; + } +} diff --git a/models/BoardGameModel.ts b/models/BoardGameModel.ts new file mode 100644 index 00000000..c65599b9 --- /dev/null +++ b/models/BoardGameModel.ts @@ -0,0 +1,64 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type BoardGameData = ModelToData; + +export class BoardGameModel extends MediaTypeModel { + genres: string[]; + onlineRating: number; + complexityRating: number; + minPlayers: number; + maxPlayers: number; + playtime: string; + publishers: string[]; + image?: string; + + released: boolean; + + userData: { + played: boolean; + personalRating: number; + }; + + constructor(obj: BoardGameData) { + super(); + + this.genres = []; + this.onlineRating = 0; + this.complexityRating = 0; + this.minPlayers = 0; + this.maxPlayers = 0; + this.playtime = ''; + this.publishers = []; + this.image = ''; + + this.released = false; + + this.userData = { + played: false, + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'boardgame']; + } + + getMediaType(): MediaType { + return MediaType.BoardGame; + } + + getSummary(): string { + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); + } +} diff --git a/models/BookModel.ts b/models/BookModel.ts new file mode 100644 index 00000000..e2f52e48 --- /dev/null +++ b/models/BookModel.ts @@ -0,0 +1,64 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type BookData = ModelToData; + +export class BookModel extends MediaTypeModel { + author: string; + plot: string; + pages: number; + image: string; + onlineRating: number; + isbn: number; + isbn13: number; + + released: boolean; + + userData: { + read: boolean; + lastRead: string; + personalRating: number; + }; + + constructor(obj: BookData) { + super(); + + this.author = ''; + this.plot = ''; + this.pages = 0; + this.image = ''; + this.onlineRating = 0; + this.isbn = 0; + this.isbn13 = 0; + + this.released = false; + + this.userData = { + read: false, + lastRead: '', + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'book']; + } + + getMediaType(): MediaType { + return MediaType.Book; + } + + getSummary(): string { + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : '') + ' - ' + this.author; + } +} diff --git a/models/ComicMangaModel.ts b/models/ComicMangaModel.ts new file mode 100644 index 00000000..0d4c64a3 --- /dev/null +++ b/models/ComicMangaModel.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 ComicMangaData = ModelToData; + +export class ComicMangaModel extends MediaTypeModel { + plot: string; + alternateTitles: string[]; + genres: string[]; + authors: string[]; + chapters: number; + volumes: number; + onlineRating: number; + image: string; + + released: boolean; + status: string; + publishers: string[]; + publishedFrom: string; + publishedTo: string; + + userData: { + read: boolean; + lastRead: string; + personalRating: number; + }; + + constructor(obj: ComicMangaData) { + super(); + + this.plot = ''; + this.alternateTitles = []; + this.genres = []; + this.authors = []; + this.chapters = 0; + this.volumes = 0; + this.onlineRating = 0; + this.image = ''; + + this.released = false; + this.status = ''; + this.publishers = []; + this.publishedFrom = ''; + this.publishedTo = ''; + + this.userData = { + read: false, + lastRead: '', + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + const tags = [mediaDbTag]; + if (this.subType) { + tags.push(this.subType); + } else { + tags.push('comicManga'); + } + return tags; + } + + getMediaType(): MediaType { + return MediaType.ComicManga; + } + + getSummary(): string { + return this.title + (this.year > 0 ? ` (${this.year})` : ''); + } +} diff --git a/models/GameModel.ts b/models/GameModel.ts new file mode 100644 index 00000000..90b972a2 --- /dev/null +++ b/models/GameModel.ts @@ -0,0 +1,68 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type GameData = ModelToData; + +export class GameModel extends MediaTypeModel { + developers: string[]; + publishers: string[]; + genres: string[]; + onlineRating: number; + image: string; + summary: string; + series: string[]; + gameModes: string[]; + platforms: string[]; + + released: boolean; + releaseDate: string; + + userData: { + played: boolean; + personalRating: number; + }; + + constructor(obj: GameData) { + super(); + + this.developers = []; + this.publishers = []; + this.genres = []; + this.onlineRating = 0; + this.image = ''; + this.summary = ''; + this.series = []; + this.gameModes = []; + this.platforms = []; + + this.released = false; + this.releaseDate = ''; + + this.userData = { + played: false, + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'game']; + } + + getMediaType(): MediaType { + return MediaType.Game; + } + + getSummary(): string { + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); + } +} diff --git a/models/MediaTypeModel.ts b/models/MediaTypeModel.ts new file mode 100644 index 00000000..e7e100b1 --- /dev/null +++ b/models/MediaTypeModel.ts @@ -0,0 +1,49 @@ +import type { MediaType } from '../utils/MediaType'; + +export abstract class MediaTypeModel { + type: string; + subType: string; + title: string; + englishTitle: string; + year: number; + dataSource: string; + url: string; + id: string; + image?: string; + + userData: object; + + protected constructor() { + this.type = ''; + this.subType = ''; + this.title = ''; + this.englishTitle = ''; + this.year = 0; + this.dataSource = ''; + this.url = ''; + this.id = ''; + this.image = ''; + + this.userData = {}; + } + + abstract getMediaType(): MediaType; + + //a string that contains enough info to disambiguate from similar media + abstract getSummary(): string; + + abstract getTags(): string[]; + + toMetaDataObject(): Record { + const obj: Record = { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; + // year: 0 means "unknown" — write null so YAML shows blank (None) instead of 0 + if (obj['year'] === 0) obj['year'] = null; + return obj; + } + + getWithOutUserData(): Record { + const copy = structuredClone(this) as Record; + delete copy.userData; + return copy; + } +} diff --git a/models/MovieModel.ts b/models/MovieModel.ts new file mode 100644 index 00000000..08b7a1b9 --- /dev/null +++ b/models/MovieModel.ts @@ -0,0 +1,88 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { coerceMovieDurationMinutes, mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type MovieData = ModelToData; + +export class MovieModel extends MediaTypeModel { + japaneseTitle: string; + plot: string; + genres: string[]; + director: string[]; + writer: string[]; + studio: string[]; + /** Total runtime in minutes. */ + duration: number; + onlineRating: number; + actors: string[]; + image: string; + + released: boolean; + country: string[]; + language: string[]; + /** Production budget in USD (e.g. from TMDB). */ + budget: string; + /** Box-office gross (e.g. worldwide from TMDB; OMDb US figure when from IMDb). */ + revenue: string; + ageRating: string; + streamingServices: string[]; + premiere: string; + + userData: { + watched: boolean; + lastWatched: string; + personalRating: number; + }; + + constructor(obj: MovieData) { + super(); + + this.japaneseTitle = ''; + this.plot = ''; + this.genres = []; + this.director = []; + this.writer = []; + this.studio = []; + this.duration = 0; + this.onlineRating = 0; + this.actors = []; + this.image = ''; + + this.released = false; + this.country = []; + this.language = []; + this.budget = ''; + this.revenue = ''; + this.ageRating = ''; + this.streamingServices = []; + this.premiere = ''; + + this.userData = { + watched: false, + lastWatched: '', + personalRating: 0, + }; + + migrateObject(this, obj, this); + this.duration = coerceMovieDurationMinutes(this.duration as unknown); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'tv', 'movie']; + } + + getMediaType(): MediaType { + return MediaType.Movie; + } + + getSummary(): string { + return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); + } +} diff --git a/models/MusicReleaseModel.ts b/models/MusicReleaseModel.ts new file mode 100644 index 00000000..3c3d6634 --- /dev/null +++ b/models/MusicReleaseModel.ts @@ -0,0 +1,69 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type MusicReleaseData = ModelToData; + +export class MusicReleaseModel extends MediaTypeModel { + genres: string[]; + artists: string[]; + language: string; + image: string; + rating: number; + releaseDate: string; + albumDuration: string; + trackCount: number; + tracks: { + number: number; + title: string; + duration: string; + featuredArtists: string[]; + /** MusicBrainz recording MBID; used to resolve Spotify and other links. */ + recordingId?: string; + }[]; + + userData: { + personalRating: number; + }; + + constructor(obj: MusicReleaseData) { + super(); + + this.genres = []; + this.artists = []; + this.image = ''; + this.rating = 0; + this.releaseDate = ''; + + this.userData = { + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + this.albumDuration = obj.albumDuration ?? '0:00'; + this.trackCount = obj.trackCount ?? 0; + this.tracks = obj.tracks ?? []; + this.language = obj.language ?? ''; + } + + getTags(): string[] { + return [mediaDbTag, 'music', this.subType]; + } + + getMediaType(): MediaType { + return MediaType.MusicRelease; + } + + getSummary(): string { + let summary = this.title + (this.year > 0 ? ` (${this.year})` : ''); + if (this.artists.length > 0) summary += ' - ' + this.artists.join(', '); + return summary; + } +} diff --git a/models/SeasonModel.ts b/models/SeasonModel.ts new file mode 100644 index 00000000..fc4e0a3a --- /dev/null +++ b/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 (!Object.hasOwn(obj, '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 this.seasonNumber + ' seasons'; + } +} diff --git a/models/SeriesModel.ts b/models/SeriesModel.ts new file mode 100644 index 00000000..4ca1662b --- /dev/null +++ b/models/SeriesModel.ts @@ -0,0 +1,86 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type SeriesData = ModelToData; + +export class SeriesModel extends MediaTypeModel { + japaneseTitle: string; + plot: string; + genres: string[]; + writer: string[]; + studio: string[]; + episodes: number; + duration: string; + onlineRating: number; + actors: string[]; + image: string; + + released: boolean; + country: string[]; + language: string[]; + network: string[]; + ageRating: string; + streamingServices: string[]; + airing: boolean; + airedFrom: string; + airedTo: string; + + userData: { + watched: boolean; + lastWatched: string; + personalRating: number; + }; + + constructor(obj: SeriesData) { + super(); + + this.japaneseTitle = ''; + this.plot = ''; + this.genres = []; + this.writer = []; + this.studio = []; + this.episodes = 0; + this.duration = ''; + this.onlineRating = 0; + this.actors = []; + this.image = ''; + + this.released = false; + this.country = []; + this.language = []; + this.network = []; + this.ageRating = ''; + this.streamingServices = []; + this.airing = false; + this.airedFrom = ''; + this.airedTo = ''; + + this.userData = { + watched: false, + lastWatched: '', + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'tv', 'series']; + } + + getMediaType(): MediaType { + return MediaType.Series; + } + + getSummary(): string { + return this.title + (this.year > 0 ? ` (${this.year})` : ''); + } +} diff --git a/models/SongModel.ts b/models/SongModel.ts new file mode 100644 index 00000000..6c45c8aa --- /dev/null +++ b/models/SongModel.ts @@ -0,0 +1,84 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type SongData = ModelToData; + +export class SongModel extends MediaTypeModel { + genres: string[]; + artists: string[]; + albumTitle: string; + albumReleaseGroupId: string; + trackNumber: number; + duration: string; + featuredArtists: string[]; + geniusUrl: string; + /** Open track URL from MusicBrainz (e.g. https://open.spotify.com/track/…) when available. */ + spotifyUrl: string; + lyrics: string; + image: string; + releaseDate: string; + + userData: { + personalRating: number; + }; + + constructor(obj: SongData) { + super(); + + this.genres = []; + this.artists = []; + this.albumTitle = ''; + this.albumReleaseGroupId = ''; + this.trackNumber = 0; + this.duration = ''; + this.featuredArtists = []; + this.geniusUrl = ''; + this.spotifyUrl = ''; + this.lyrics = ''; + this.image = ''; + this.releaseDate = ''; + + this.userData = { + personalRating: 0, + }; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + this.trackNumber = obj.trackNumber ?? 0; + this.albumTitle = obj.albumTitle ?? ''; + this.albumReleaseGroupId = obj.albumReleaseGroupId ?? ''; + this.duration = obj.duration ?? ''; + this.featuredArtists = obj.featuredArtists ?? []; + this.geniusUrl = obj.geniusUrl ?? ''; + this.spotifyUrl = obj.spotifyUrl ?? ''; + this.lyrics = obj.lyrics ?? ''; + this.releaseDate = obj.releaseDate ?? ''; + } + + getTags(): string[] { + return [mediaDbTag, 'music', 'song']; + } + + getMediaType(): MediaType { + return MediaType.Song; + } + + getSummary(): string { + const albumPart = this.albumTitle ? ` — ${this.albumTitle}` : ''; + const artists = this.artists.length > 0 ? this.artists.join(', ') : ''; + return `${this.title}${albumPart}${artists ? ` (${artists})` : ''}`; + } + + getWithOutUserData(): Record { + const copy = super.getWithOutUserData(); + delete copy.lyrics; + return copy; + } +} diff --git a/models/WikiModel.ts b/models/WikiModel.ts new file mode 100644 index 00000000..ee49c2dc --- /dev/null +++ b/models/WikiModel.ts @@ -0,0 +1,52 @@ +import { MediaType } from '../utils/MediaType'; +import type { ModelToData } from '../utils/Utils'; +import { mediaDbTag, migrateObject } from '../utils/Utils'; +import { MediaTypeModel } from './MediaTypeModel'; + +export type WikiData = ModelToData; + +export class WikiModel extends MediaTypeModel { + wikiUrl: string; + lastUpdated: string; + length: number; + article: string; + + userData: Record; + + constructor(obj: WikiData) { + super(); + + this.wikiUrl = ''; + this.lastUpdated = ''; + this.length = 0; + this.article = ''; + this.userData = {}; + + migrateObject(this, obj, this); + + if (!Object.hasOwn(obj, 'userData')) { + migrateObject(this.userData, obj, this.userData); + } + + this.type = this.getMediaType(); + } + + getTags(): string[] { + return [mediaDbTag, 'wiki']; + } + + getMediaType(): MediaType { + return MediaType.Wiki; + } + + override getWithOutUserData(): Record { + const copy = structuredClone(this) as Record; + delete copy.userData; + delete copy.article; + return copy; + } + + getSummary(): string { + return this.title; + } +} diff --git a/settings/Icon.tsx b/settings/Icon.tsx new file mode 100644 index 00000000..97231973 --- /dev/null +++ b/settings/Icon.tsx @@ -0,0 +1,24 @@ +import { onMount, Show } from 'solid-js'; +import { setIcon } from 'obsidian'; + +interface IconProps { + iconName?: string; +} + +export default function Icon(props: IconProps) { + let iconEl: HTMLDivElement | undefined; + + onMount(() => { + if (iconEl) { + setIcon(iconEl, props.iconName || ''); + } + }); + + return ( + 0}> +
+
+
+
+ ); +} diff --git a/settings/PropertyMapper.ts b/settings/PropertyMapper.ts new file mode 100644 index 00000000..1ff0c809 --- /dev/null +++ b/settings/PropertyMapper.ts @@ -0,0 +1,209 @@ +import type MediaDbPlugin from '../main'; +import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; +import { PropertyMappingOption } from './PropertyMapping'; + +export class PropertyMapper { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + /** + * Converts an object using the conversion rules for its type. + * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. + * + * @param obj + */ + convertObject(obj: Record): Record { + if (!Object.hasOwn(obj, 'type')) { + return obj; + } + + // console.log(obj.type); + + const internalMediaType = resolveMetadataTypeToMediaType(this.plugin.settings, obj.type); + if (!internalMediaType) { + return obj; + } + + const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === internalMediaType); + if (!propertyMappingModel) { + return obj; + } + + const propertyMappings = propertyMappingModel.properties; + + const newObj: Record = {}; + + const entityProps = this.plugin.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + + // 1. Preprocess global wiki-links on the raw object first + if (this.plugin.settings.enableWikiLinkParsing && entityProps.length > 0) { + for (const [key, value] of Object.entries(obj)) { + if (key === 'aliases') continue; + if (entityProps.includes(key.toLowerCase())) { + const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; + const formatWiki = (v: unknown) => { + if (typeof v !== 'string') return v; + let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); + if (clean.includes('|')) clean = clean.split('|')[1]; + return `[[${folderPrefix}${clean}|${clean}]]`; + }; + + if (typeof value === 'string') { + obj[key] = formatWiki(value); + } else if (Array.isArray(value)) { + obj[key] = value.map(formatWiki); + } + } + } + } + + // 2. Map standard properties + for (const [key, value] of Object.entries(obj)) { + if (key === 'aliases') { + continue; + } + for (const propertyMapping of propertyMappings) { + if (propertyMapping.property === key) { + let finalValue = value; + if (propertyMapping.wikilink) { + const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; + // Resolve the originating API so it can provide property-specific link formatting + const api = typeof obj.dataSource === 'string' + ? this.plugin.apiManager.getApiByName(obj.dataSource) + : undefined; + const wikilink = (v: unknown): unknown => { + if (typeof v !== 'string') return v; + if (api) return api.wikilinkValueFor(key, v, obj, folderPrefix); + const clean = v.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; + return `[[${folderPrefix}${clean}|${clean}]]`; + }; + if (typeof value === 'string') { + finalValue = wikilink(value); + } else if (Array.isArray(value)) { + finalValue = value.map(wikilink); + } + } + if (propertyMapping.mapping === PropertyMappingOption.Map) { + // @ts-ignore + newObj[propertyMapping.newProperty] = finalValue; + } else if (propertyMapping.mapping === PropertyMappingOption.Remove) { + // do nothing + } else if (propertyMapping.mapping === PropertyMappingOption.Default) { + // @ts-ignore + newObj[key] = finalValue; + } + break; + } + } + } + + if (Object.hasOwn(obj, 'aliases')) { + const aliasesPm = propertyMappings.find(p => p.property === 'aliases'); + if (aliasesPm?.mapping !== PropertyMappingOption.Remove) { + const incoming = obj['aliases']; + const targetKey = + aliasesPm?.mapping === PropertyMappingOption.Map && aliasesPm.newProperty + ? aliasesPm.newProperty + : 'aliases'; + const merged = PropertyMapper.mergeAliasValues(newObj[targetKey], incoming); + if (merged.length > 0) { + newObj[targetKey] = merged; + } + } + } + + return newObj; + } + + private static mergeAliasValues(existing: unknown, added: unknown): string[] { + const toStrings = (v: unknown): string[] => { + if (v == null) { + return []; + } + if (Array.isArray(v)) { + return v.flatMap(x => (typeof x === 'string' ? x : String(x))).filter(s => s.length > 0); + } + if (typeof v === 'string') { + return v.length > 0 ? [v] : []; + } + return []; + }; + + const combined = [...toStrings(existing), ...toStrings(added)]; + const seen = new Set(); + const out: string[] = []; + for (const s of combined) { + if (!seen.has(s)) { + seen.add(s); + out.push(s); + } + } + return out; + } + + /** + * Converts an object back using the conversion rules for its type. + * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. + * + * @param obj + */ + convertObjectBack(obj: Record): Record { + const models = this.plugin.settings.propertyMappingModels; + + let matchedModel: (typeof models)[number] | undefined; + for (const model of models) { + const typePm = model.properties.find(p => p.property === 'type'); + const typeKey = + typePm?.mapping === PropertyMappingOption.Map && typePm.newProperty + ? typePm.newProperty + : 'type'; + if (!Object.hasOwn(obj, typeKey)) { + continue; + } + let typeVal: unknown = obj[typeKey]; + if (typeVal === 'manga') { + typeVal = 'comicManga'; + console.debug(`MDB | updated metadata type`, typeVal); + } + const typeStr = String(typeVal).trim(); + if ( + typeStr === (model.type as string) || + typeStr === noteTypeValueForMedia(this.plugin.settings, model.type) + ) { + matchedModel = model; + break; + } + } + + if (!matchedModel) { + return obj; + } + + const propertyMappings = matchedModel.properties; + const originalObj: Record = {}; + + objLoop: for (const [key, value] of Object.entries(obj)) { + for (const propertyMapping of propertyMappings) { + if (propertyMapping.property === key) { + originalObj[key] = value; + continue objLoop; + } + } + for (const propertyMapping of propertyMappings) { + if ( + propertyMapping.mapping === PropertyMappingOption.Map && + propertyMapping.newProperty === key + ) { + originalObj[propertyMapping.property] = value; + continue objLoop; + } + } + } + + return originalObj; + } + +} diff --git a/settings/PropertyMapping.ts b/settings/PropertyMapping.ts new file mode 100644 index 00000000..649320e9 --- /dev/null +++ b/settings/PropertyMapping.ts @@ -0,0 +1,277 @@ +import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; +import type { MediaType } from '../utils/MediaType'; +import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; + +// Plain object interfaces for serialization +export interface PropertyMappingData { + property: string; + newProperty: string; + mapping: PropertyMappingOption; + locked?: boolean; + wikilink?: boolean; +} + +export interface PropertyMappingModelData { + type: MediaType; + properties: PropertyMappingData[]; +} + +export enum PropertyMappingOption { + Default = 'default', + Map = 'remap', + Remove = 'remove', +} + +export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; + +const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id'] as const; + +export class PropertyMappingModel { + type: MediaType; + properties: PropertyMapping[]; + + constructor(type: MediaType, properties?: PropertyMapping[]) { + this.type = type; + this.properties = properties ?? []; + } + + validate(): { res: boolean; err?: Error } { + console.debug(`MDB | validated property mappings for ${this.type}`); + + // check properties + for (const property of this.properties) { + const propertyValidation = property.validate(); + if (!propertyValidation.res) { + return { + res: false, + err: propertyValidation.err, + }; + } + } + + // check for name collisions + for (const property of this.getMappedProperties()) { + const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty); + if (propertiesWithSameTarget.length === 0) { + // if we get there, then something in this code is wrong + } else if (propertiesWithSameTarget.length === 1) { + // all good + } else { + // two or more properties are mapped to the same property + return { + res: false, + err: new PropertyMappingNameConflictError( + `Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`, + ), + }; + } + } + // remapped properties may not have the same name as any original property + for (const property of this.getMappedProperties()) { + const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property); + if (propertiesWithSameTarget.length === 0) { + // all good + } else { + // a mapped property shares the same name with an original property + return { + res: false, + err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`), + }; + } + } + + const dataSourceRule = this.properties.find(p => p.property === 'dataSource'); + if (dataSourceRule?.mapping === PropertyMappingOption.Remove && !musicBrainzRegisteredApiName(this.type)) { + return { + res: false, + err: new PropertyMappingValidationError( + `Removing dataSource is only allowed for artist, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, + ), + }; + } + + return { + res: true, + }; + } + + getMappedProperties(): PropertyMapping[] { + return this.properties.filter(x => x.mapping === PropertyMappingOption.Map); + } + + copy(): PropertyMappingModel { + const copy = new PropertyMappingModel(this.type); + for (const property of this.properties) { + const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked, property.wikilink); + copy.properties.push(propertyCopy); + } + return copy; + } + + // Serialization - returns a plain object that can be JSON.stringify'd + toJSON(): PropertyMappingModelData { + return { + type: this.type, + properties: this.properties.map(p => p.toJSON()), + }; + } + + // Deserialization - creates a PropertyMappingModel from a plain object + static fromJSON(json: PropertyMappingModelData): PropertyMappingModel { + return new PropertyMappingModel( + json.type, + json.properties.map(p => PropertyMapping.fromJSON(p)), + ); + } + + /** + * Migrates loaded settings to match the structure of default settings. + * - Adds new properties from defaults that don't exist in loaded settings + * - Preserves user customizations from loaded settings + * - Updates locked status from defaults + * + * @param loadedModels - Models loaded from disk (may be outdated) + * @param defaultModels - Current default models (source of truth for structure) + * @returns Migrated models with correct structure and preserved user settings + */ + static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { + const migratedModels: PropertyMappingModel[] = []; + + for (const defaultModel of defaultModels) { + const loadedModel = loadedModels.find(m => m.type === defaultModel.type); + + if (!loadedModel) { + // New model type - use default + migratedModels.push(defaultModel); + continue; + } + + // Migrate properties + const migratedProperties: PropertyMapping[] = []; + for (const defaultProperty of defaultModel.properties) { + const loadedProperty = loadedModel.properties.find(p => p.property === defaultProperty.property); + + if (!loadedProperty) { + // New property - use default + migratedProperties.push(defaultProperty); + } else { + // Existing property - merge: take locked from default, customizations from loaded + migratedProperties.push( + new PropertyMapping( + loadedProperty.property, + loadedProperty.newProperty, + loadedProperty.mapping, + defaultProperty.locked, // locked status from default + loadedProperty.wikilink ?? false, + ), + ); + } + } + + migratedModels.push(new PropertyMappingModel(defaultModel.type, migratedProperties)); + } + + return migratedModels; + } +} + +export class PropertyMapping { + property: string; + newProperty: string; + locked: boolean; + mapping: PropertyMappingOption; + wikilink: boolean; + + constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean) { + this.property = property; + this.newProperty = newProperty; + this.mapping = mapping; + this.locked = locked ?? false; + this.wikilink = wikilink ?? false; + } + + validate(): { res: boolean; err?: Error } { + // locked property may only be default + if (this.locked) { + if (this.mapping === PropertyMappingOption.Remove) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`), + }; + } + if (this.mapping === PropertyMappingOption.Map) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`), + }; + } + } + + if ( + (METADATA_KEYS_REQUIRED_IN_NOTE as readonly string[]).includes(this.property) && + this.mapping === PropertyMappingOption.Remove + ) { + return { + res: false, + err: new PropertyMappingValidationError( + `Error in property mapping "${this.toString()}": type and id must appear in the note (you can remap them, but not remove them).`, + ), + }; + } + + if (this.mapping === PropertyMappingOption.Default) { + return { res: true }; + } + if (this.mapping === PropertyMappingOption.Remove) { + return { res: true }; + } + + if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and may only contain letters and underscores.`), + }; + } + + if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) { + return { + res: false, + err: new PropertyMappingValidationError( + `Error in property mapping "${this.toString()}": new property may not be empty and may only contain letters and underscores.`, + ), + }; + } + + return { + res: true, + }; + } + + toString(): string { + if (this.mapping === PropertyMappingOption.Default) { + return this.property; + } else if (this.mapping === PropertyMappingOption.Map) { + return `${this.property} -> ${this.newProperty}`; + } else if (this.mapping === PropertyMappingOption.Remove) { + return `remove ${this.property}`; + } + + return this.property; + } + + // Serialization - returns a plain object + toJSON(): PropertyMappingData { + return { + property: this.property, + newProperty: this.newProperty, + mapping: this.mapping, + locked: this.locked, + wikilink: this.wikilink, + }; + } + + // Deserialization - creates a PropertyMapping from a plain object + static fromJSON(json: PropertyMappingData): PropertyMapping { + return new PropertyMapping(json.property, json.newProperty, json.mapping, json.locked, json.wikilink); + } +} diff --git a/settings/PropertyMappingModelComponent.tsx b/settings/PropertyMappingModelComponent.tsx new file mode 100644 index 00000000..a533687a --- /dev/null +++ b/settings/PropertyMappingModelComponent.tsx @@ -0,0 +1,128 @@ +import { createMemo, For, Show } from 'solid-js'; +import { createStore } from 'solid-js/store'; +import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; +import Icon from './Icon'; + +interface PropertyMappingModelComponentProps { + model: PropertyMappingModelData; + save: (model: PropertyMappingModelData) => void; + /** When false, hides the media-type heading (e.g. modal title already shows it). Default true. */ + showMediaTypeTitle?: boolean; +} + +export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { + // Create a store from the model's plain data + const [modelData, setModelData] = createStore(props.model); + + // Derive the validation result reactively + const validationResult = createMemo(() => { + const model = PropertyMappingModel.fromJSON(modelData); + return model.validate(); + }); + + const persistIfValid = () => { + const model = PropertyMappingModel.fromJSON(modelData); + if (model.validate().res) { + props.save(model); + } + }; + + const showTitle = () => props.showMediaTypeTitle !== false; + + return ( +
+ +
+
{mediaTypeDisplayName(modelData.type as MediaType)}
+
+
+ + +
{validationResult().err?.message}
+
+ +
+ + + + + + + + + + + + {(property, index) => ( + + + + +
property cannot be remapped
+ + } + > +
+ + + + + + + )} + + +
PropertyMappingNew nameWikilink
+ {property.property} + + + + —} + > +
+ + { + setModelData('properties', index(), 'newProperty', e.currentTarget.value); + persistIfValid(); + }} + /> +
+
+
+
+
+ ); +} diff --git a/settings/Settings.ts b/settings/Settings.ts new file mode 100644 index 00000000..8b62bb28 --- /dev/null +++ b/settings/Settings.ts @@ -0,0 +1,1160 @@ +import type { App, IconName } from 'obsidian'; +import { Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; +import { MediaType } from 'src/utils/MediaType'; +import type MediaDbPlugin from '../main'; +import { ApiSecretID } from './apiSecretsHelper'; +import { PropertyMappingModal } from '../modals/PropertyMappingModal'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import { MEDIA_TYPES } from '../utils/MediaTypeManager'; +import { noteTypeValueForMedia, setNoteTypeForMedia } from '../utils/noteTypeSettings'; +import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; +import type { PropertyMappingModelData } from './PropertyMapping'; +import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; +import { FileSuggest } from './suggesters/FileSuggest'; +import { FolderSuggest } from './suggesters/FolderSuggest'; + + +function mediaTypeTabIcon(mediaType: MediaType): IconName { + switch (mediaType) { + case MediaType.Artist: + return 'mic-2'; + case MediaType.BoardGame: + return 'dice-3'; + case MediaType.Book: + return 'book-marked'; + case MediaType.ComicManga: + return 'book-open'; + case MediaType.Game: + return 'gamepad-2'; + case MediaType.Movie: + return 'film'; + case MediaType.MusicRelease: + return 'disc-3'; + case MediaType.Season: + return 'calendar-range'; + case MediaType.Series: + return 'tv'; + case MediaType.Song: + return 'music-4'; + case MediaType.Wiki: + return 'library-big'; + } +} + +// MARK: Settings +export interface MediaDbPluginSettings { + sfwFilter: boolean; + templates: boolean; + customDateFormat: string; + openNoteInNewTab: boolean; + useDefaultFrontMatter: boolean; + /** When true, add an Obsidian `aliases` entry with an ASCII form of the title when it uses diacritics or letters like ø (e.g. Likbør → Likbor). */ + autoTrackerAiringKey: string; + autoTrackerReleasedKey: string; + enableTemplaterIntegration: boolean; + imageDownload: boolean; + imageFolder: string; + tmdbRegion: string; + enableAutoTagging: boolean; + autoTagEntities: string; + autoTagProperties: string; + enableWikiLinkParsing: boolean; + autoUpdateAiringMode: boolean; + + BoardgameGeekAPI_disabledMediaTypes: MediaType[]; + ComicVineAPI_disabledMediaTypes: MediaType[]; + GiantBombAPI_disabledMediaTypes: MediaType[]; + IGDBAPI_disabledMediaTypes: MediaType[]; + RAWGAPI_disabledMediaTypes: MediaType[]; + MALAPI_disabledMediaTypes: MediaType[]; + MALAPIManga_disabledMediaTypes: MediaType[]; + MobyGamesAPI_disabledMediaTypes: MediaType[]; + MusicBrainzAPI_disabledMediaTypes: MediaType[]; + MusicBrainzArtistAPI_disabledMediaTypes: MediaType[]; + OMDbAPI_disabledMediaTypes: MediaType[]; + OpenLibraryAPI_disabledMediaTypes: MediaType[]; + SteamAPI_disabledMediaTypes: MediaType[]; + TMDBMovieAPI_disabledMediaTypes: MediaType[]; + TMDBSeasonAPI_disabledMediaTypes: MediaType[]; + TMDBSeriesAPI_disabledMediaTypes: MediaType[]; + VNDBAPI_disabledMediaTypes: MediaType[]; + WikipediaAPI_disabledMediaTypes: MediaType[]; + + movieTemplate: string; + seriesTemplate: string; + seasonTemplate: string; + mangaTemplate: string; + gameTemplate: string; + wikiTemplate: string; + musicReleaseTemplate: string; + artistTemplate: string; + songTemplate: string; + boardgameTemplate: string; + bookTemplate: string; + + movieFileNameTemplate: string; + seriesFileNameTemplate: string; + seasonFileNameTemplate: string; + mangaFileNameTemplate: string; + gameFileNameTemplate: string; + wikiFileNameTemplate: string; + musicReleaseFileNameTemplate: string; + artistFileNameTemplate: string; + songFileNameTemplate: string; + boardgameFileNameTemplate: string; + bookFileNameTemplate: string; + + movieFolder: string; + seriesFolder: string; + seasonFolder: string; + mangaFolder: string; + gameFolder: string; + wikiFolder: string; + musicReleaseFolder: string; + artistFolder: string; + songFolder: string; + + /** Frontmatter `type` for each media kind (empty = default internal id, e.g. movie, musicRelease). */ + movieNoteType: string; + seriesNoteType: string; + seasonNoteType: string; + mangaNoteType: string; + gameNoteType: string; + wikiNoteType: string; + musicReleaseNoteType: string; + artistNoteType: string; + songNoteType: string; + boardgameNoteType: string; + bookNoteType: string; + /** When true, artist discography import nests albums and songs under artistFolder/ArtistName/… instead of using album/song import folders. */ + artistUseFileTreeForSongs: boolean; + boardgameFolder: string; + bookFolder: string; + + propertyMappingModels: PropertyMappingModelData[]; + linkedApiSecretIds: Record; +} + +/** + * Helper class to get/set settings for a specific media type. + */ +class MediaTypeMappedSettings { + mediaType: MediaType; + + constructor(mediaType: MediaType) { + this.mediaType = mediaType; + } + + getTemplate(settings: MediaDbPluginSettings): string { + switch (this.mediaType) { + case MediaType.Artist: + return settings.artistTemplate; + case MediaType.BoardGame: + return settings.boardgameTemplate; + case MediaType.Book: + return settings.bookTemplate; + case MediaType.ComicManga: + return settings.mangaTemplate; + case MediaType.Game: + return settings.gameTemplate; + case MediaType.Movie: + return settings.movieTemplate; + case MediaType.MusicRelease: + return settings.musicReleaseTemplate; + case MediaType.Season: + return settings.seasonTemplate; + case MediaType.Series: + return settings.seriesTemplate; + case MediaType.Song: + return settings.songTemplate; + case MediaType.Wiki: + return settings.wikiTemplate; + } + } + + setTemplate(settings: MediaDbPluginSettings, template: string): void { + switch (this.mediaType) { + case MediaType.Artist: + settings.artistTemplate = template; + break; + case MediaType.BoardGame: + settings.boardgameTemplate = template; + break; + case MediaType.Book: + settings.bookTemplate = template; + break; + case MediaType.ComicManga: + settings.mangaTemplate = template; + break; + case MediaType.Game: + settings.gameTemplate = template; + break; + case MediaType.Movie: + settings.movieTemplate = template; + break; + case MediaType.MusicRelease: + settings.musicReleaseTemplate = template; + break; + case MediaType.Season: + settings.seasonTemplate = template; + break; + case MediaType.Series: + settings.seriesTemplate = template; + break; + case MediaType.Song: + settings.songTemplate = template; + break; + case MediaType.Wiki: + settings.wikiTemplate = template; + break; + } + } + + getFileNameTemplate(settings: MediaDbPluginSettings): string { + switch (this.mediaType) { + case MediaType.Artist: + return settings.artistFileNameTemplate; + case MediaType.BoardGame: + return settings.boardgameFileNameTemplate; + case MediaType.Book: + return settings.bookFileNameTemplate; + case MediaType.ComicManga: + return settings.mangaFileNameTemplate; + case MediaType.Game: + return settings.gameFileNameTemplate; + case MediaType.Movie: + return settings.movieFileNameTemplate; + case MediaType.MusicRelease: + return settings.musicReleaseFileNameTemplate; + case MediaType.Season: + return settings.seasonFileNameTemplate; + case MediaType.Series: + return settings.seriesFileNameTemplate; + case MediaType.Song: + return settings.songFileNameTemplate; + case MediaType.Wiki: + return settings.wikiFileNameTemplate; + } + } + + setFileNameTemplate(settings: MediaDbPluginSettings, template: string): void { + switch (this.mediaType) { + case MediaType.Artist: + settings.artistFileNameTemplate = template; + break; + case MediaType.BoardGame: + settings.boardgameFileNameTemplate = template; + break; + case MediaType.Book: + settings.bookFileNameTemplate = template; + break; + case MediaType.ComicManga: + settings.mangaFileNameTemplate = template; + break; + case MediaType.Game: + settings.gameFileNameTemplate = template; + break; + case MediaType.Movie: + settings.movieFileNameTemplate = template; + break; + case MediaType.MusicRelease: + settings.musicReleaseFileNameTemplate = template; + break; + case MediaType.Season: + settings.seasonFileNameTemplate = template; + break; + case MediaType.Series: + settings.seriesFileNameTemplate = template; + break; + case MediaType.Song: + settings.songFileNameTemplate = template; + break; + case MediaType.Wiki: + settings.wikiFileNameTemplate = template; + break; + } + } + + getFolder(settings: MediaDbPluginSettings): string { + switch (this.mediaType) { + case MediaType.Artist: + return settings.artistFolder; + case MediaType.BoardGame: + return settings.boardgameFolder; + case MediaType.Book: + return settings.bookFolder; + case MediaType.ComicManga: + return settings.mangaFolder; + case MediaType.Game: + return settings.gameFolder; + case MediaType.Movie: + return settings.movieFolder; + case MediaType.MusicRelease: + return settings.musicReleaseFolder; + case MediaType.Season: + return settings.seasonFolder; + case MediaType.Series: + return settings.seriesFolder; + case MediaType.Song: + return settings.songFolder; + case MediaType.Wiki: + return settings.wikiFolder; + } + } + + setFolder(settings: MediaDbPluginSettings, folder: string): void { + switch (this.mediaType) { + case MediaType.Artist: + settings.artistFolder = folder; + break; + case MediaType.BoardGame: + settings.boardgameFolder = folder; + break; + case MediaType.Book: + settings.bookFolder = folder; + break; + case MediaType.ComicManga: + settings.mangaFolder = folder; + break; + case MediaType.Game: + settings.gameFolder = folder; + break; + case MediaType.Movie: + settings.movieFolder = folder; + break; + case MediaType.MusicRelease: + settings.musicReleaseFolder = folder; + break; + case MediaType.Season: + settings.seasonFolder = folder; + break; + case MediaType.Series: + settings.seriesFolder = folder; + break; + case MediaType.Song: + settings.songFolder = folder; + break; + case MediaType.Wiki: + settings.wikiFolder = folder; + break; + } + } + + getNoteType(settings: MediaDbPluginSettings): string { + const configured = noteTypeValueForMedia(settings, this.mediaType); + return configured === this.mediaType ? '' : configured; + } + + setNoteType(settings: MediaDbPluginSettings, value: string): void { + const trimmed = value.trim(); + if (trimmed === '' || trimmed === this.mediaType) { + setNoteTypeForMedia(settings, this.mediaType, ''); + return; + } + setNoteTypeForMedia(settings, this.mediaType, value); + } +} + +// MARK: Defaults +const DEFAULT_SETTINGS: MediaDbPluginSettings = { + sfwFilter: true, + templates: true, + customDateFormat: 'L', + openNoteInNewTab: true, + useDefaultFrontMatter: true, + autoTrackerAiringKey: 'airing', + autoTrackerReleasedKey: 'released', + enableTemplaterIntegration: false, + imageDownload: false, + imageFolder: 'Media DB/images', + enableAutoTagging: false, + autoTagEntities: '', + autoTagProperties: '', + enableWikiLinkParsing: false, + autoUpdateAiringMode: false, + tmdbRegion: 'US', + + BoardgameGeekAPI_disabledMediaTypes: [], + ComicVineAPI_disabledMediaTypes: [], + GiantBombAPI_disabledMediaTypes: [], + IGDBAPI_disabledMediaTypes: [], + RAWGAPI_disabledMediaTypes: [], + MALAPI_disabledMediaTypes: [], + MALAPIManga_disabledMediaTypes: [], + MobyGamesAPI_disabledMediaTypes: [], + MusicBrainzAPI_disabledMediaTypes: [], + MusicBrainzArtistAPI_disabledMediaTypes: [], + OMDbAPI_disabledMediaTypes: [], + OpenLibraryAPI_disabledMediaTypes: [], + SteamAPI_disabledMediaTypes: [], + TMDBMovieAPI_disabledMediaTypes: [], + TMDBSeasonAPI_disabledMediaTypes: [], + TMDBSeriesAPI_disabledMediaTypes: [], + VNDBAPI_disabledMediaTypes: [], + WikipediaAPI_disabledMediaTypes: [], + + movieTemplate: '', + seriesTemplate: '', + seasonTemplate: '', + mangaTemplate: '', + gameTemplate: '', + wikiTemplate: '', + musicReleaseTemplate: '', + artistTemplate: '', + songTemplate: '', + boardgameTemplate: '', + bookTemplate: '', + + movieFileNameTemplate: '{{ title }} ({{ year }})', + seriesFileNameTemplate: '{{ title }} ({{ year }})', + seasonFileNameTemplate: '{{ title }} ({{ year }})', + mangaFileNameTemplate: '{{ title }} ({{ year }})', + gameFileNameTemplate: '{{ title }} ({{ year }})', + wikiFileNameTemplate: '{{ title }}', + musicReleaseFileNameTemplate: '{{ title }} ({{ FIRST:artists }} - {{ year }})', + artistFileNameTemplate: '{{ title }}', + songFileNameTemplate: '{{ trackNumber }}. {{ title }} ({{ albumTitle }})', + boardgameFileNameTemplate: '{{ title }} ({{ year }})', + bookFileNameTemplate: '{{ title }} ({{ year }})', + + movieFolder: 'Media DB/movies', + seriesFolder: 'Media DB/series', + seasonFolder: 'Media DB/series', + mangaFolder: 'Media DB/comics', + gameFolder: 'Media DB/games', + wikiFolder: 'Media DB/wiki', + musicReleaseFolder: 'Media DB/music', + artistFolder: 'Media DB/artists', + songFolder: 'Media DB/music/songs', + artistUseFileTreeForSongs: false, + boardgameFolder: 'Media DB/boardgames', + bookFolder: 'Media DB/books', + + movieNoteType: '', + seriesNoteType: '', + seasonNoteType: '', + mangaNoteType: '', + gameNoteType: '', + wikiNoteType: '', + musicReleaseNoteType: '', + artistNoteType: '', + songNoteType: '', + boardgameNoteType: '', + bookNoteType: '', + + propertyMappingModels: [], + + linkedApiSecretIds: { + [ApiSecretID.omdb]: '', + [ApiSecretID.tmdb]: '', + [ApiSecretID.mobyGames]: '', + [ApiSecretID.giantBomb]: '', + [ApiSecretID.igdbClientId]: '', + [ApiSecretID.igdbClientSecret]: '', + [ApiSecretID.rawg]: '', + [ApiSecretID.comicVine]: '', + [ApiSecretID.boardgameGeek]: '', + [ApiSecretID.genius]: '', + [ApiSecretID.spotifyClientId]: '', + [ApiSecretID.spotifyClientSecret]: '', + }, +}; + +export const lockedPropertyMappings: string[] = []; + +export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings { + const defaultSettings = DEFAULT_SETTINGS; + + // construct property mapping defaults + const propertyMappingModels: PropertyMappingModelData[] = []; + for (const mediaType of MEDIA_TYPES) { + const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType); + const metadataObj = model.toMetaDataObject(); + + const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType); + + for (const key of Object.keys(metadataObj)) { + propertyMappingModel.properties.push( + new PropertyMapping( + key, + '', + PropertyMappingOption.Default, + lockedPropertyMappings.contains(key), + false, // wikilink default + ), + ); + } + + // Convert to plain data for serialization + propertyMappingModels.push(propertyMappingModel.toJSON()); + } + + defaultSettings.propertyMappingModels = propertyMappingModels; + return defaultSettings; +} + +interface MediaDbSettingsTabNavEntry { + id: string; + nav: HTMLElement; + panel: HTMLElement; +} + +/** Stable order for property-mapping UI and persisted settings (`MEDIA_TYPES`; settings tabs move Board game last). */ +export function propertyMappingModelsInDisplayOrder(models: PropertyMappingModelData[]): PropertyMappingModelData[] { + const order = new Map(MEDIA_TYPES.map((t, i) => [t, i])); + return [...models].sort((a, b) => (order.get(a.type) ?? 999) - (order.get(b.type) ?? 999)); +} + +// MARK: Settings Tab +export class MediaDbSettingTab extends PluginSettingTab { + plugin: MediaDbPlugin; + private activeSettingsTabId: string | null = null; + + constructor(app: App, plugin: MediaDbPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + private addApiSecretSetting(group: SettingGroup, name: string, description: string, slot: ApiSecretID): void { + group.addSetting( + setting => + setting + .setName(name) + .setDesc(description) + .addComponent(el => { + const component = new SecretComponent(this.app, el); + const { linkedApiSecretIds } = this.plugin.settings; + const linkedId = linkedApiSecretIds[slot] ?? ''; + component.setValue(linkedId).onChange((secretId: string) => { + linkedApiSecretIds[slot] = secretId; + this.plugin.saveSettings(); + }); + return component; + }), + ); + } + + private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Artist, MediaType.MusicRelease, MediaType.Song]; + + private static readonly BOOK_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Book, MediaType.ComicManga]; + + private static readonly VIDEO_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Season]; + + private renderMediaTypeSection( + panel: HTMLElement, + mediaTypeSetting: MediaTypeMappedSettings, + mediaTypeApiMap: Map, + options?: { + sectionHeading?: string; + hideImportFolder?: boolean; + appendToSection?: (group: SettingGroup) => void; + }, + ): void { + const mediaType = mediaTypeSetting.mediaType; + const descNoun = options?.sectionHeading?.toLowerCase() ?? mediaTypeDisplayName(mediaType).toLowerCase(); + + if (options?.sectionHeading) { + panel.createEl('h3', { text: options.sectionHeading }); + } + + const mediaTypeGroup = new SettingGroup(panel); + + if (!options?.hideImportFolder) { + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Import folder') + .setDesc(`Where newly imported ${descNoun} notes should be placed.`) + .addSearch(cb => { + const suggester = new FolderSuggest(this.app, cb.inputEl); + suggester.onSelect(folder => { + cb.setValue(folder.path); + mediaTypeSetting.setFolder(this.plugin.settings, folder.path); + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) + .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setFolder(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + } + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Note type') + .setDesc( + `Value for the "type" field in frontmatter. Leave blank to use the default (${mediaType}).`, + ) + .addText(cb => { + cb.setPlaceholder(String(mediaType)) + .setValue(mediaTypeSetting.getNoteType(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setNoteType(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Template') + .setDesc(`Template file used when creating a new ${descNoun} note.`) + .addSearch(cb => { + const suggester = new FileSuggest(this.app, cb.inputEl); + suggester.onSelect(file => { + cb.setValue(file.path); + mediaTypeSetting.setTemplate(this.plugin.settings, file.path); + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(`Example: ${descNoun.replace(/ /g, '')}Template.md`) + .setValue(mediaTypeSetting.getTemplate(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setTemplate(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName('File name template') + .setDesc(`File name template for new ${descNoun} notes.`) + .addText(cb => { + cb.setPlaceholder(`Example: ${mediaTypeSetting.getFileNameTemplate(DEFAULT_SETTINGS)}`) + .setValue(mediaTypeSetting.getFileNameTemplate(this.plugin.settings)) + .onChange(data => { + mediaTypeSetting.setFileNameTemplate(this.plugin.settings, data); + void this.plugin.saveSettings(); + }); + }), + ); + + const apis = mediaTypeApiMap.get(mediaType) ?? []; + if (apis.length > 1) { + for (const apiName of apis) { + const api = this.plugin.apiManager.apis.find(a => a.apiName === apiName); + if (api) { + const disabledMediaTypes = api.getDisabledMediaTypes(); + + mediaTypeGroup.addSetting( + setting => + void setting + .setName(apiName) + .setDesc(`Use ${apiName} for ${descNoun} search and import.`) + .addToggle(cb => { + cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => { + if (data) { + const index = disabledMediaTypes.indexOf(mediaType); + if (index != -1) { + disabledMediaTypes.splice(index, 1); + } + } else { + disabledMediaTypes.push(mediaType); + } + void this.plugin.saveSettings(); + }); + }), + ); + } + } + } + + options?.appendToSection?.(mediaTypeGroup); + + if (this.plugin.settings.useDefaultFrontMatter) { + mediaTypeGroup.addSetting(setting => + void setting + .setName('Property mappings') + .setDesc(`How metadata fields map to frontmatter for ${descNoun} notes.`) + .addButton(btn => { + btn.setButtonText('Edit'); + btn.onClick(() => { + new PropertyMappingModal(this.app, this.plugin, mediaType).open(); + }); + }), + ); + } + } + + private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + const fileTree = this.plugin.settings.artistUseFileTreeForSongs; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Artist), mediaTypeApiMap, { + sectionHeading: 'Artist', + appendToSection: group => { + group.addSetting( + setting => + void setting + .setName('Use file trees for songs') + .setDesc( + 'Use a file tree hierarchy to store albums and songs for each artist.', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.artistUseFileTreeForSongs).onChange(data => { + this.plugin.settings.artistUseFileTreeForSongs = data; + void this.plugin.saveSettings(); + this.display(); + }); + }), + ); + }, + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { + sectionHeading: 'Album', + hideImportFolder: fileTree, + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { + sectionHeading: 'Song', + hideImportFolder: fileTree, + }); + } + + private renderBookSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Book), mediaTypeApiMap, { + sectionHeading: 'Book', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.ComicManga), mediaTypeApiMap, { + sectionHeading: 'Comic & Manga', + }); + } + + private renderVideoSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { + const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + + this.renderMediaTypeSection(panel, byType(MediaType.Movie), mediaTypeApiMap, { + sectionHeading: 'Movie', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Series), mediaTypeApiMap, { + sectionHeading: 'Series', + }); + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + this.renderMediaTypeSection(panel, byType(MediaType.Season), mediaTypeApiMap, { + sectionHeading: 'Season', + }); + + panel.createDiv({ cls: 'media-db-plugin-spacer' }); + panel.createEl('h3', { text: 'Region' }); + const regionGroup = new SettingGroup(panel); + regionGroup.addSetting( + setting => + void setting + .setName('TMDB Region') + .setDesc('ISO-3166-1 region code for TMDB localized metadata (e.g., US, TR, GB). Default is US.') + .addText(text => + text + .setPlaceholder('US') + .setValue(this.plugin.settings.tmdbRegion) + .onChange(async value => { + this.plugin.settings.tmdbRegion = value; + await this.plugin.saveSettings(); + }), + ), + ); + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + const headerNav = containerEl.createEl('nav', { cls: 'media-db-setting-header' }); + const tabGroup = headerNav.createDiv({ cls: 'media-db-setting-tab-group' }); + const settingsContentEl = containerEl.createDiv({ cls: 'media-db-setting-content' }); + + const tabEntries: MediaDbSettingsTabNavEntry[] = []; + + const selectTab = (id: string): void => { + for (const { id: tid, nav, panel } of tabEntries) { + const on = tid === id; + panel.toggleClass('media-db-tab-settings--hidden', !on); + nav.toggleClass('media-db-navigation-item-selected', on); + } + this.activeSettingsTabId = id; + }; + + const addTab = (id: string, title: string, icon: IconName, render: (panel: HTMLElement) => void): void => { + const nav = tabGroup.createDiv({ cls: 'media-db-navigation-item' }); + nav.addClass(Platform.isMobile ? 'media-db-mobile' : 'media-db-desktop'); + setIcon(nav.createSpan({ cls: 'media-db-navigation-item-icon' }), icon); + nav.createSpan().setText(title); + const panel = settingsContentEl.createDiv({ cls: 'media-db-tab-settings media-db-tab-settings--hidden' }); + render(panel); + tabEntries.push({ id, nav, panel }); + nav.addEventListener('click', () => selectTab(id)); + }; + + const mediaTypeSettings = [ + ...MEDIA_TYPES.filter(mt => mt !== MediaType.BoardGame).map(mt => new MediaTypeMappedSettings(mt)), + new MediaTypeMappedSettings(MediaType.BoardGame), + ]; + + const mediaTypeApiMap = new Map(); + for (const api of this.plugin.apiManager.apis) { + for (const mediaType of api.types) { + if (!mediaTypeApiMap.has(mediaType)) { + mediaTypeApiMap.set(mediaType, []); + } + mediaTypeApiMap.get(mediaType)!.push(api.apiName); + } + } + + addTab('general', 'General', 'sliders-horizontal', panel => { + const generalGroup = new SettingGroup(panel); + + generalGroup.addSetting( + setting => + void setting + .setName('SFW filter') + .setDesc('Only shows SFW results for APIs that offer filtering.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.sfwFilter).onChange(data => { + this.plugin.settings.sfwFilter = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Resolve {{ tags }} in templates') + .setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.templates).onChange(data => { + this.plugin.settings.templates = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Date format') + .setDesc( + fragWithHTML( + "Your custom date format. Use 'YYYY-MM-DD' for example.
" + + "For more syntax, refer to format reference.
" + + "Your current syntax looks like this: " + + this.plugin.dateFormatter.getPreview() + + "", + ), + ) + .addText(cb => { + cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) + .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) + .onChange(data => { + const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; + this.plugin.settings.customDateFormat = newDateFormat; + const previewEl = document.getElementById('media-db-dateformat-preview'); + if (previewEl) { + previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview + } + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Open note in new tab') + .setDesc('Open the newly created note in a new tab.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => { + this.plugin.settings.openNoteInNewTab = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Use default front matter') + .setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { + this.plugin.settings.useDefaultFrontMatter = data; + void this.plugin.saveSettings(); + this.display(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Enable Templater integration') + .setDesc('Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { + this.plugin.settings.enableTemplaterIntegration = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Download images') + .setDesc('Downloads images for new notes in the folder below') + .addToggle(cb => { + cb.setValue(this.plugin.settings.imageDownload).onChange(data => { + this.plugin.settings.imageDownload = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Image folder') + .setDesc('Where downloaded images should be stored.') + .addSearch(cb => { + const suggester = new FolderSuggest(this.app, cb.inputEl); + suggester.onSelect(folder => { + cb.setValue(folder.path); + this.plugin.settings.imageFolder = folder.path; + void this.plugin.saveSettings(); + suggester.close(); + }); + cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder) + .setValue(this.plugin.settings.imageFolder) + .onChange(data => { + this.plugin.settings.imageFolder = data; + void this.plugin.saveSettings(); + }); + }), + ); + + panel.createEl('h3', { text: 'Auto-Tracker' }).style.marginTop = '1.5em'; + const autoTrackerGroup = new SettingGroup(panel); + + autoTrackerGroup.addSetting( + setting => + void setting + .setName('Auto-Update Airing & Unreleased Media') + .setDesc('At startup, automatically searches background for any active medias with "released: false" or "airing: true" and updates them via API.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.autoUpdateAiringMode).onChange(data => { + this.plugin.settings.autoUpdateAiringMode = data; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTrackerGroup.addSetting( + setting => + void setting + .setName('Auto-Tracker "Airing" Property') + .setDesc('Property key to check if a media item is currently airing. Default is "airing".') + .addText(text => { + text.setValue(this.plugin.settings.autoTrackerAiringKey).onChange(data => { + this.plugin.settings.autoTrackerAiringKey = data.trim() || 'airing'; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTrackerGroup.addSetting( + setting => + void setting + .setName('Auto-Tracker "Released" Property') + .setDesc('Property key to check if a media item is unreleased. Default is "released".') + .addText(text => { + text.setValue(this.plugin.settings.autoTrackerReleasedKey).onChange(data => { + this.plugin.settings.autoTrackerReleasedKey = data.trim() || 'released'; + void this.plugin.saveSettings(); + }); + }), + ); + + panel.createEl('h3', { text: 'Auto-Tag Properties' }).style.marginTop = '1.5em'; + const autoTagGroup = new SettingGroup(panel); + + autoTagGroup.addSetting( + setting => + void setting + .setName('Enable Auto Tagging') + .setDesc('Feature to automatically sanitize properties into standard Obsidian tags.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableAutoTagging).onChange(data => { + this.plugin.settings.enableAutoTagging = data; + void this.plugin.saveSettings(); + }); + }), + ); + + autoTagGroup.addSetting( + setting => + void setting + .setName('Auto-Tag whitelisted properties') + .setDesc('Comma separated list of property names. If a property in this list is present, its values will be sanitized and appended to the Obsidian native `tags` array.') + .addText(text => + text + .setPlaceholder('genres, platforms') + .setValue(this.plugin.settings.autoTagProperties) + .onChange(async value => { + this.plugin.settings.autoTagProperties = value; + await this.plugin.saveSettings(); + }), + ), + ); + }); + + // Render individual media type tabs + // Game tab + const renderGameTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { + this.renderMediaTypeSection(panel, setting, mediaTypeApiMap); + }; + + // Wiki tab — inject Wiki-Link settings + const renderWikiTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { + this.renderMediaTypeSection(panel, setting, mediaTypeApiMap, { + appendToSection: group => { + group.addSetting( + s => + void s + .setName('Wiki-Link parsing') + .setDesc('When enabled, properties listed below are formatted as Obsidian [[Wiki-Links]] across ALL media types globally. This complements the per-property wikilink checkbox in Property Mappings, which only affects that specific property.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.enableWikiLinkParsing).onChange(data => { + this.plugin.settings.enableWikiLinkParsing = data; + void this.plugin.saveSettings(); + }); + }), + ); + group.addSetting( + s => + void s + .setName('Wiki-Link properties') + .setDesc('Comma-separated property names to convert to [[Wiki-Links]] for ALL media types. Use this for custom or cross-type properties (e.g. storefront, launcher). For standard properties like genres or studio, the wikilink checkbox inside Property Mappings also works.') + .addTextArea(cb => { + cb.setPlaceholder('genres, storefront, category') + .setValue(this.plugin.settings.autoTagEntities) + .onChange(value => { + this.plugin.settings.autoTagEntities = value; + void this.plugin.saveSettings(); + }); + }), + ); + }, + }); + }; + + addTab('api-keys', 'API keys', 'key', panel => { + const apiKeyGroup = new SettingGroup(panel); + + this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', ApiSecretID.omdb); + this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', ApiSecretID.tmdb); + + this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', ApiSecretID.mobyGames); + this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', ApiSecretID.giantBomb); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', ApiSecretID.igdbClientId); + this.addApiSecretSetting(apiKeyGroup, 'IGDB Client Secret', 'Client Secret for IGDB API.', ApiSecretID.igdbClientSecret); + this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', ApiSecretID.rawg); + this.addApiSecretSetting(apiKeyGroup, 'Comic Vine Key', 'API key for "www.comicvine.gamespot.com".', ApiSecretID.comicVine); + this.addApiSecretSetting(apiKeyGroup, 'Boardgame Geek Key', 'API key for "www.boardgamegeek.com".', ApiSecretID.boardgameGeek); + this.addApiSecretSetting( + apiKeyGroup, + 'Genius API access token', + 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing an artist.', + ApiSecretID.genius, + ); + this.addApiSecretSetting( + apiKeyGroup, + 'Spotify Client ID', + 'From https://developer.spotify.com/dashboard — used to resolve track links when MusicBrainz has no Spotify URL (with Client Secret).', + ApiSecretID.spotifyClientId, + ); + this.addApiSecretSetting( + apiKeyGroup, + 'Spotify Client Secret', + 'Pair with Spotify Client ID for client-credentials access to search tracks during artist import.', + ApiSecretID.spotifyClientSecret, + ); + }); + + let musicTabAdded = false; + let bookTabAdded = false; + let videoTabAdded = false; + for (const mediaTypeSetting of mediaTypeSettings) { + const mediaType = mediaTypeSetting.mediaType; + + if (MediaDbSettingTab.MUSIC_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!musicTabAdded) { + musicTabAdded = true; + addTab('media-music', 'Music', 'disc-3', panel => { + this.renderMusicSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); + } + continue; + } + + if (MediaDbSettingTab.BOOK_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!bookTabAdded) { + bookTabAdded = true; + addTab('media-book', 'Book', mediaTypeTabIcon(MediaType.Book), panel => { + this.renderBookSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); + } + continue; + } + + if (MediaDbSettingTab.VIDEO_SETTINGS_MEDIA_TYPES.includes(mediaType)) { + if (!videoTabAdded) { + videoTabAdded = true; + addTab('media-movie', 'Movie', mediaTypeTabIcon(MediaType.Movie), panel => { + this.renderVideoSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); + }); + } + continue; + } + + const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); + if (mediaType === MediaType.Game) { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + renderGameTab(panel, mediaTypeSetting); + }); + } else if (mediaType === MediaType.Wiki) { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + renderWikiTab(panel, mediaTypeSetting); + }); + } else { + addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { + this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); + }); + } + } + + const validIds = new Set(tabEntries.map(t => t.id)); + let initialId = this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; + if (!validIds.has(initialId)) { + initialId = 'general'; + } + selectTab(initialId); + } +} diff --git a/settings/apiSecretsHelper.ts b/settings/apiSecretsHelper.ts new file mode 100644 index 00000000..b20908b7 --- /dev/null +++ b/settings/apiSecretsHelper.ts @@ -0,0 +1,28 @@ +import type { App } from 'obsidian'; + +/** + * Settings slots for API credentials. Each slot stores the Obsidian SecretStorage **id** + * the user selects (including via SecretComponent → Link), not the raw secret. + * @see https://docs.obsidian.md/plugins/guides/secret-storage + */ +export enum ApiSecretID { + omdb, + tmdb, + mobyGames, + giantBomb, + igdbClientId, + igdbClientSecret, + rawg, + comicVine, + boardgameGeek, + genius, + /** Spotify Developer Dashboard — used when MusicBrainz has no streaming URL for a recording. */ + spotifyClientId, + spotifyClientSecret, +} + +export function getApiSecretValue(app: App, linked: Record | undefined, slot: ApiSecretID): string { + const id = linked?.[slot] ?? ''; + if (id === '') return ''; + return app.secretStorage.getSecret(id) ?? ''; +} diff --git a/settings/suggesters/FileSuggest.ts b/settings/suggesters/FileSuggest.ts new file mode 100644 index 00000000..6bbdb968 --- /dev/null +++ b/settings/suggesters/FileSuggest.ts @@ -0,0 +1,17 @@ +import { AbstractInputSuggest, TFile } from 'obsidian'; + +export class FileSuggest extends AbstractInputSuggest { + protected getSuggestions(query: string): TFile[] | Promise { + const lowerCaseInputStr = query.toLowerCase(); + + // we do two filters because otherwise TS type inference does convert the array to TFile[] + return this.app.vault + .getAllLoadedFiles() + .filter(file => file instanceof TFile) + .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); + } + + renderSuggestion(value: TFile, el: HTMLElement): void { + el.setText(value.path); + } +} diff --git a/settings/suggesters/FolderSuggest.ts b/settings/suggesters/FolderSuggest.ts new file mode 100644 index 00000000..eaee7153 --- /dev/null +++ b/settings/suggesters/FolderSuggest.ts @@ -0,0 +1,17 @@ +import { AbstractInputSuggest, TFolder } from 'obsidian'; + +export class FolderSuggest extends AbstractInputSuggest { + protected getSuggestions(query: string): TFolder[] | Promise { + const lowerCaseInputStr = query.toLowerCase(); + + // we do two filters because otherwise TS type inference does convert the array to TFolder[] + return this.app.vault + .getAllLoadedFiles() + .filter(file => file instanceof TFolder) + .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); + } + + renderSuggestion(value: TFolder, el: HTMLElement): void { + el.setText(value.path); + } +} diff --git a/styles.css b/styles.css index 9b9260d6..5c0c2e6a 100644 --- a/styles.css +++ b/styles.css @@ -1,21 +1,438 @@ +.media-db-plugin-list-wrapper { + display: flex; + align-content: center; + margin-bottom: 5px; + margin-top: 5px; +} + +.media-db-plugin-list-toggle { +} + +.media-db-plugin-list-text-wrapper { + flex: 1; +} + +.media-db-plugin-list-text { + display: block; +} + +small.media-db-plugin-list-text { + color: var(--text-muted); +} + +.media-db-plugin-select-modal { + display: contents; +} + +.media-db-plugin-select-wrapper { + display: flex; + flex-direction: column; + margin: 5px; + overflow-y: auto; +} + +.media-db-plugin-select-element { + cursor: pointer; + border-left: 5px solid transparent; + padding: 5px; + margin: 5px 0 5px 0; + border-radius: 5px; + white-space: pre-wrap; + font-size: 16px; +} + +.media-db-plugin-select-element-selected { + border-left: 5px solid var(--interactive-accent) !important; + background: var(--background-secondary-alt); +} + +.media-db-plugin-select-element-hover { + background: var(--background-secondary-alt); +} + +.media-db-plugin-preview-modal { + display: contents; +} + +.media-db-plugin-preview-wrapper { + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.media-db-plugin-spacer { + margin-bottom: 10px; +} + +.media-db-plugin-button:focus { + /*outline: 1px solid white;*/ +} + +.media-db-plugin-preview { + border-radius: var(--modal-radius); + border: var(--modal-border-width) solid var(--modal-border-color); + padding: var(--size-4-4); +} + +/* Icon Component Styles */ +.icon-wrapper { + display: inline-block; + position: relative; + width: 20px; +} + +.icon { + position: absolute; + height: 20px; + width: 20px; + top: calc(50% - 10px); +} + +/* Property Mapping Component Styles */ +.media-db-plugin-property-mappings-model-container { + margin-bottom: var(--size-4-8); +} + +.media-db-plugin-property-mappings-model-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--size-4-4); + gap: var(--size-4-3); +} + +.media-db-plugin-property-mappings-model-header--actions-only { + justify-content: flex-end; +} + +.media-db-plugin-property-mappings-model-header .setting-item-name { + font-weight: var(--font-semibold); + font-size: var(--font-ui-medium); + color: var(--text-normal); + margin: 0; +} + +.media-db-plugin-property-mappings-model-actions { + display: flex; + align-items: center; + gap: var(--size-4-3); +} + +.media-db-plugin-property-mapping-unsaved-changes { + color: var(--text-warning); + font-size: var(--font-ui-small); + white-space: nowrap; +} + +.media-db-plugin-property-mappings-save-button { + white-space: nowrap; + cursor: pointer; +} + +.media-db-plugin-property-mappings-save-button.mod-muted { + opacity: 0.5; + cursor: not-allowed; +} + +.media-db-plugin-property-mapping-validation { + color: var(--text-error); + background: rgba(var(--color-red-rgb), 0.1); + padding: var(--size-4-3) var(--size-4-4); + margin-bottom: var(--size-4-4); + border-left: 3px solid var(--text-error); + font-size: var(--font-ui-small); + line-height: 1.5; + border-radius: var(--radius-s); +} + +.media-db-plugin-property-mappings-table-container { + overflow-x: auto; +} + +.media-db-plugin-property-mappings-table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + font-size: var(--font-ui-small); +} + +.media-db-plugin-property-mappings-table thead { + border-bottom: 1px solid var(--background-modifier-border); +} + +.media-db-plugin-property-mappings-table th { + padding: var(--size-4-2) var(--size-4-3); + padding-left: 0; + text-align: left; + font-weight: var(--font-semibold); + color: var(--text-muted); + font-size: var(--font-ui-smaller); + text-transform: uppercase; + letter-spacing: 0.02em; + border-bottom: none; +} + +.media-db-plugin-property-mappings-table tbody tr { + transition: background-color 0.1s ease; +} + +.media-db-plugin-property-mappings-table td { + padding: var(--size-4-3) var(--size-4-3) var(--size-4-3) 0; + border-bottom: 1px solid var(--background-modifier-border-hover); + vertical-align: middle; +} + +.media-db-plugin-property-mappings-table tbody tr:last-child td { + border-bottom: none; +} + +.col-property { + width: 25%; + white-space: nowrap; +} + +.col-mapping { + width: 20%; +} + +.col-new-name { + width: 40%; +} + +.col-wikilink { + width: 15%; + text-align: center; +} + +.col-locked { + text-align: center; + font-style: italic; +} + +.media-db-plugin-property-mappings-table code { + padding: var(--size-4-1) var(--size-4-2); + margin: 0; + background: var(--code-background); + color: var(--code-normal); + border-radius: var(--radius-s); + font-size: var(--font-ui-smaller); + font-family: var(--font-monospace); +} + +.media-db-plugin-property-binding-text { + color: var(--text-muted); + font-size: var(--font-ui-small); + font-style: italic; +} + +.media-db-plugin-property-mappings-table select.dropdown { + width: 100%; + max-width: 100%; +} + +.media-db-plugin-property-mapping-to { + display: flex; + align-items: center; + gap: var(--size-4-2); + min-width: 0; +} + +.media-db-plugin-property-mapping-input { + flex: 1; + width: 100%; + font-family: var(--font-monospace); +} + +.media-db-plugin-property-mapping-to-disabled { + color: var(--text-faint); + font-size: var(--font-ui-medium); +} + +.media-db-plugin-property-mapping-wikilink-label { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: var(--size-4-1); +} + +.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'] { + cursor: pointer; + width: var(--checkbox-size); + height: var(--checkbox-size); +} + /* -------------------------------------------- -Media DB - Release Build -------------------------------------------- -By: Moritz Jung (https://www.moritzjung.dev) -Time: Sun, 29 Mar 2026 07:47:20 GMT -Version: 0.8.0 -------------------------------------------- -THIS IS A GENERATED/BUNDLED FILE -if you want to view the source, please visit the github repository of this plugin -------------------------------------------- -Copyright (c) 2026 Moritz Jung -------------------------------------------- -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . -*/ - -.media-db-plugin-list-wrapper{display:flex;align-content:center;margin-bottom:5px;margin-top:5px}.media-db-plugin-list-text-wrapper{flex:1}.media-db-plugin-list-text{display:block}small.media-db-plugin-list-text{color:var(--text-muted)}.media-db-plugin-select-modal{display:contents}.media-db-plugin-select-wrapper{display:flex;flex-direction:column;margin:5px;overflow-y:auto}.media-db-plugin-select-element{cursor:pointer;border-left:5px solid transparent;padding:5px;margin:5px 0;border-radius:5px;white-space:pre-wrap;font-size:16px}.media-db-plugin-select-element-selected{border-left:5px solid var(--interactive-accent)!important;background:var(--background-secondary-alt)}.media-db-plugin-select-element-hover{background:var(--background-secondary-alt)}.media-db-plugin-preview-modal{display:contents}.media-db-plugin-preview-wrapper{display:flex;flex-direction:column;overflow-y:auto}.media-db-plugin-spacer{margin-bottom:10px}.media-db-plugin-preview{border-radius:var(--modal-radius);border:var(--modal-border-width) solid var(--modal-border-color);padding:var(--size-4-4)}.icon-wrapper{display:inline-block;position:relative;width:20px}.icon{position:absolute;height:20px;width:20px;top:calc(50% - 10px)}.media-db-plugin-property-mappings-model-container{margin-bottom:var(--size-4-8)}.media-db-plugin-property-mappings-model-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--size-4-4);gap:var(--size-4-3)}.media-db-plugin-property-mappings-model-header .setting-item-name{font-weight:var(--font-semibold);font-size:var(--font-ui-medium);color:var(--text-normal);margin:0}.media-db-plugin-property-mappings-model-actions{display:flex;align-items:center;gap:var(--size-4-3)}.media-db-plugin-property-mapping-unsaved-changes{color:var(--text-warning);font-size:var(--font-ui-small);white-space:nowrap}.media-db-plugin-property-mappings-save-button{white-space:nowrap;cursor:pointer}.media-db-plugin-property-mappings-save-button.mod-muted{opacity:.5;cursor:not-allowed}.media-db-plugin-property-mapping-validation{color:var(--text-error);background:rgba(var(--color-red-rgb),.1);padding:var(--size-4-3) var(--size-4-4);margin-bottom:var(--size-4-4);border-left:3px solid var(--text-error);font-size:var(--font-ui-small);line-height:1.5;border-radius:var(--radius-s)}.media-db-plugin-property-mappings-table-container{overflow-x:auto}.media-db-plugin-property-mappings-table{width:100%;border-collapse:collapse;border-spacing:0;font-size:var(--font-ui-small)}.media-db-plugin-property-mappings-table thead{border-bottom:1px solid var(--background-modifier-border)}.media-db-plugin-property-mappings-table th{padding:var(--size-4-2) var(--size-4-3);padding-left:0;text-align:left;font-weight:var(--font-semibold);color:var(--text-muted);font-size:var(--font-ui-smaller);text-transform:uppercase;letter-spacing:.02em;border-bottom:none}.media-db-plugin-property-mappings-table tbody tr{transition:background-color .1s ease}.media-db-plugin-property-mappings-table td{padding:var(--size-4-3) var(--size-4-3) var(--size-4-3) 0;border-bottom:1px solid var(--background-modifier-border-hover);vertical-align:middle}.media-db-plugin-property-mappings-table tbody tr:last-child td{border-bottom:none}.col-property{width:25%;white-space:nowrap}.col-mapping{width:20%}.col-new-name{width:40%}.col-wikilink{width:15%;text-align:center}.col-locked{text-align:center;font-style:italic}.media-db-plugin-property-mappings-table code{padding:var(--size-4-1) var(--size-4-2);margin:0;background:var(--code-background);color:var(--code-normal);border-radius:var(--radius-s);font-size:var(--font-ui-smaller);font-family:var(--font-monospace)}.media-db-plugin-property-binding-text{color:var(--text-muted);font-size:var(--font-ui-small);font-style:italic}.media-db-plugin-property-mappings-table select.dropdown{width:100%;max-width:100%}.media-db-plugin-property-mapping-to{display:flex;align-items:center;gap:var(--size-4-2);min-width:0}.media-db-plugin-property-mapping-input{flex:1;width:100%;font-family:var(--font-monospace)}.media-db-plugin-property-mapping-to-disabled{color:var(--text-faint);font-size:var(--font-ui-medium)}.media-db-plugin-property-mapping-wikilink-label{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:var(--size-4-1)}.media-db-plugin-property-mapping-wikilink-label input[type=checkbox]{cursor:pointer;width:var(--checkbox-size);height:var(--checkbox-size)} + * Settings tabs: same layout pattern as Obsidian Linter (horizontal icon tabs). + * Adapted from https://github.com/platers/obsidian-linter/blob/master/src/styles.css (MIT). + */ +.media-db-navigation-item { + cursor: pointer; + border-radius: 8px 8px 2px 2px; + border: 1px solid var(--background-modifier-border); + font-weight: bold; + font-size: 16px; + display: flex; + flex-direction: row; + white-space: nowrap; + padding: 4px 6px; + align-items: center; + gap: 4px; + overflow: hidden; + background-color: var(--background-primary-secondary-alt); + transition: + color 0.25s ease-in-out, + padding 0.25s ease-in-out, + background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), + max-width 0.35s cubic-bezier(0.57, 0.04, 0.58, 1); + height: 32px; +} + +@media screen and (max-width: 1325px) { + .media-db-navigation-item.media-db-desktop { + max-width: 32px; + } +} + +@media screen and (max-width: 800px) { + .media-db-navigation-item.media-db-mobile { + max-width: 32px; + } +} + +.media-db-navigation-item-icon { + padding-top: 5px; +} + +.media-db-navigation-item:hover { + border-color: var(--interactive-accent-hover); + border-bottom: 0; +} + +.media-db-navigation-item-selected { + background-color: var(--interactive-accent) !important; + color: var(--text-on-accent); + padding: 4px 9px !important; + max-width: 100% !important; + border: 1px solid var(--background-modifier-border); + border-radius: 8px 8px 2px 2px; + border-bottom: 0; + transition: + color 0.25s ease-in-out, + padding 0.25s ease-in-out, + background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), + max-width 0.45s cubic-bezier(0.57, 0.04, 0.58, 1) 0.2s; +} + +.media-db-setting-header { + margin-bottom: 24px; + overflow-y: hidden; + overflow-x: auto; +} + +.media-db-setting-header .media-db-setting-tab-group { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + width: 100%; +} + +.media-db-setting-tab-group { + margin-top: 6px; + padding-left: 2px; + padding-right: 2px; + border-bottom: 2px solid var(--background-modifier-border); +} + +.media-db-tab-settings--hidden { + display: none !important; +} + +.media-db-navigation-item:not(.media-db-navigation-item-selected) > span:nth-child(2), +.media-db-visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} + +/* ── Completion Modal ─────────────────────────────── */ +.mdb-completion-modal { + padding: 8px 4px 4px; + min-width: 280px; +} + +.mdb-completion-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.mdb-completion-icon { + font-size: 1.6em; + line-height: 1; +} + +.mdb-completion-title { + margin: 0; + font-size: 1.15em; + font-weight: 600; + color: var(--text-normal); +} + +.mdb-completion-stats { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 24px; +} + +.mdb-completion-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 7px 0; + border-bottom: 1px solid var(--background-modifier-border); +} + +.mdb-completion-label { + color: var(--text-muted); + font-size: 0.92em; +} + +.mdb-completion-value { + font-weight: 600; + color: var(--text-normal); + font-size: 0.95em; +} + +.mdb-stat-success { color: var(--color-green); } +.mdb-stat-error { color: var(--color-red); } +.mdb-stat-skipped { color: var(--text-muted); } + +.mdb-completion-notes { + margin-bottom: 20px; + padding: 8px 12px; + background: var(--background-secondary); + border-radius: var(--radius-s); + font-size: 0.88em; + color: var(--text-muted); +} + +.mdb-completion-notes p { + margin: 0; +} + +.mdb-completion-footer { + display: flex; + justify-content: flex-end; + padding-top: 12px; +} diff --git a/utils/AutoTrackerHelper.ts b/utils/AutoTrackerHelper.ts new file mode 100644 index 00000000..7155c9dd --- /dev/null +++ b/utils/AutoTrackerHelper.ts @@ -0,0 +1,90 @@ +import { Notice, TFile, TFolder } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { CompletionModal } from 'src/modals/CompletionModal'; +import { dateTimeToString, markdownTable } from './Utils'; + +export class AutoTrackerHelper { + readonly plugin: MediaDbPlugin; + public isScanning: boolean = false; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async startBackgroundScan(silent: boolean = false, targetFolder?: TFolder): Promise { + if (this.isScanning) return; + this.isScanning = true; + this.plugin.refreshAutoTrackerRibbon(); + await this.runAutoUpdate(silent, targetFolder); + this.isScanning = false; + this.plugin.refreshAutoTrackerRibbon(); + } + + async runAutoUpdate(silent: boolean = false, targetFolder?: TFolder): Promise { + const allFiles = targetFolder + ? this.plugin.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(targetFolder.path)) + : this.plugin.app.vault.getMarkdownFiles(); + + const filesToUpdate: TFile[] = []; + + for (const file of allFiles) { + const metadata = this.plugin.getMetadataFromFileCache(file); + if (metadata && metadata.dataSource && metadata.id) { + const airingKey = this.plugin.settings.autoTrackerAiringKey; + const releasedKey = this.plugin.settings.autoTrackerReleasedKey; + if (metadata[airingKey] === true || metadata[releasedKey] === false) { + filesToUpdate.push(file); + } + } + } + + if (filesToUpdate.length === 0) { + if (!silent) { + new Notice('MDB Tracker | No airing or unreleased media found to update.'); + } + return; + } + + const noticeMsg = `MDB Tracker | Found ${filesToUpdate.length} ongoing/unreleased notes. Updating in background...`; + if (!silent) { + new Notice(noticeMsg); + } + console.log(noticeMsg); + + const startTime = Date.now(); + let successCount = 0; + let failCount = 0; + const erroredFiles: { filePath: string, error: string }[] = []; + + for (const file of filesToUpdate) { + try { + await this.plugin.updateNote(file, true, false, silent); + successCount++; + } catch (e) { + console.warn(`MDB Tracker | Failed to auto-update ${file.path}: `, e); + failCount++; + erroredFiles.push({ filePath: file.path, error: `${e}` }); + } + // Sleep longer (1s) to be completely safe during background checks + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + if (failCount > 0 && erroredFiles.length > 0) { + const title = `MDB - auto tracker error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } + + new CompletionModal(this.plugin.app, { + title: 'Auto Tracker Complete', + icon: '🎯', + total: filesToUpdate.length, + success: successCount, + errors: failCount, + elapsedMs: Date.now() - startTime, + notes: failCount > 0 ? ['Some notes could not be updated. A detailed report file has been created in your vault folder.'] : [], + }).open(); + } +} diff --git a/utils/BulkImportHelper.ts b/utils/BulkImportHelper.ts new file mode 100644 index 00000000..ffbe88dd --- /dev/null +++ b/utils/BulkImportHelper.ts @@ -0,0 +1,169 @@ +import type { TFolder } from 'obsidian'; +import { TFile } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; +import type { MediaTypeModel } from 'src/models/MediaTypeModel'; +import { CompletionModal } from 'src/modals/CompletionModal'; +import { ModalResultCode } from './ModalHelper'; +import { dateTimeToString, markdownTable } from './Utils'; + +export enum BulkImportLookupMethod { + ID = 'id', + TITLE = 'title', +} + +interface BulkImportError { + filePath: string; + error: string; + canceled?: boolean; +} + +export class BulkImportHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async import(folder: TFolder): Promise { + const erroredFiles: BulkImportError[] = []; + let canceled: boolean = false; + let successCount = 0; + const startTime = Date.now(); + + const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ + selectedAPI: string; + lookupMethod: BulkImportLookupMethod; + fieldName: string; + appendContent: boolean; + }>(resolve => { + new MediaDbBulkImportModal(this.plugin, (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => { + resolve({ selectedAPI, lookupMethod, fieldName, appendContent }); + }).open(); + }); + + for (const child of folder.children) { + if (!(child instanceof TFile)) { + continue; + } + + const file: TFile = child; + if (canceled) { + erroredFiles.push({ filePath: file.path, error: 'user canceled' }); + continue; + } + + const metadata = this.plugin.getMetadataFromFileCache(file); + const lookupValue = metadata[fieldName]; + + if (!lookupValue || typeof lookupValue !== 'string') { + erroredFiles.push({ filePath: file.path, error: `metadata field '${fieldName}' not found, empty, or not a string` }); + continue; + } else if (lookupMethod === BulkImportLookupMethod.ID) { + const error = await this.importById(file, lookupValue, selectedAPI, appendContent); + if (error) { + erroredFiles.push(error); + } else { + successCount++; + } + } else if (lookupMethod === BulkImportLookupMethod.TITLE) { + const error = await this.importByTitle(file, lookupValue, selectedAPI, appendContent); + if (error) { + if (error.canceled) { + canceled = true; + } + erroredFiles.push(error); + } else { + successCount++; + } + } else { + erroredFiles.push({ filePath: file.path, error: `invalid lookup type` }); + continue; + } + } + + if (erroredFiles.length > 0) { + await this.createErroredFilesReport(erroredFiles); + } + + const total = successCount + erroredFiles.length; + new CompletionModal(this.plugin.app, { + title: 'Bulk Import Complete', + icon: '📥', + total, + success: successCount, + errors: erroredFiles.filter(e => !e.canceled).length, + skipped: erroredFiles.filter(e => e.canceled).length, + elapsedMs: Date.now() - startTime, + notes: erroredFiles.length > 0 ? ['Error report saved to vault.'] : [], + }).open(); + } + + private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { + try { + const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); + if (model) { + await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined); + return undefined; + } else { + return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; + } + } catch (e) { + return { filePath: file.path, error: `${e}` }; + } + } + + private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { + let results: MediaTypeModel[] = []; + try { + results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); + } catch (e) { + return { filePath: file.path, error: `${e}` }; + } + if (!results || results.length === 0) { + return { filePath: file.path, error: `no search results` }; + } + + const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({ + elements: results, + skipButton: true, + modalTitle: `Results for '${lookupValue}'`, + }); + + if (selectModalResult.code === ModalResultCode.ERROR) { + selectModal.close(); + return { filePath: file.path, error: selectModalResult.error.message }; + } + + if (selectModalResult.code === ModalResultCode.CLOSE) { + selectModal.close(); + return { filePath: file.path, error: 'user canceled', canceled: true }; + } + + if (selectModalResult.code === ModalResultCode.SKIP) { + selectModal.close(); + return { filePath: file.path, error: 'user skipped' }; + } + + if (selectModalResult.data.selected.length === 0) { + selectModal.close(); + return { filePath: file.path, error: `no search results selected` }; + } + + const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); + await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); + + selectModal.close(); + return undefined; + } + + private async createErroredFilesReport(erroredFiles: BulkImportError[]): Promise { + const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } +} diff --git a/utils/BulkUpdateHelper.ts b/utils/BulkUpdateHelper.ts new file mode 100644 index 00000000..d5c37db3 --- /dev/null +++ b/utils/BulkUpdateHelper.ts @@ -0,0 +1,64 @@ +import { TFolder, TFile, Notice } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { BulkUpdateConfirmModal } from 'src/modals/BulkUpdateConfirmModal'; +import { CompletionModal } from 'src/modals/CompletionModal'; +import { dateTimeToString, markdownTable } from './Utils'; + +export class BulkUpdateHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async updateFolder(folder: TFolder): Promise { + const mediaFiles = folder.children.filter((child): child is TFile => { + if (!(child instanceof TFile)) return false; + const metadata = this.plugin.getMetadataFromFileCache(child); + return Boolean(metadata && metadata.dataSource && metadata.id); + }); + + if (mediaFiles.length === 0) { + new Notice('MDB | No Media DB files found in this folder.'); + return; + } + + new BulkUpdateConfirmModal(this.plugin.app, async (silent: boolean) => { + new Notice(`MDB | Bulk updating ${mediaFiles.length} files. Please wait...`); + const startTime = Date.now(); + let successCount = 0; + let failCount = 0; + const erroredFiles: { filePath: string, error: string }[] = []; + + for (const file of mediaFiles) { + try { + await this.plugin.updateNote(file, true, false, silent); + successCount++; + } catch (e) { + console.error(`MDB | Failed to bulk update ${file.path}: `, e); + failCount++; + erroredFiles.push({ filePath: file.path, error: `${e}` }); + } + await new Promise(resolve => setTimeout(resolve, 800)); + } + + if (failCount > 0 && erroredFiles.length > 0) { + const title = `MDB - bulk update error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } + + new CompletionModal(this.plugin.app, { + title: 'Bulk Update Complete', + icon: '🔄', + total: mediaFiles.length, + success: successCount, + errors: failCount, + elapsedMs: Date.now() - startTime, + notes: failCount > 0 ? ['Some files could not be updated. A detailed report file has been created in your vault folder.'] : [], + }).open(); + }).open(); + } +} diff --git a/utils/DateFormatter.ts b/utils/DateFormatter.ts new file mode 100644 index 00000000..0088af06 --- /dev/null +++ b/utils/DateFormatter.ts @@ -0,0 +1,65 @@ +import { moment } from 'obsidian'; + +export class DateFormatter { + toFormat: string; + locale: string; + + constructor() { + this.toFormat = 'YYYY-MM-DD'; + // get locale of this machine (e.g. en, en-gb, de, fr, etc.) + this.locale = new Intl.DateTimeFormat().resolvedOptions().locale; + } + + setFormat(format: string): void { + this.toFormat = format; + } + + getPreview(format?: string): string { + const today = moment(); + + format ??= this.toFormat; + + return today.locale(this.locale).format(format); + } + + /** + * Tries to format a given date string with the currently set date format. + * You can set a date format by calling `setFormat()`. + * + * @param dateString the date string to be formatted + * @param dateFormat the current format of `dateString`. When this is `null` and the actual format of the + * given date string is not `C2822` or `ISO` format, this function will try to guess the format by using the native `Date` module. + * @param locale the locale of `dateString`. This is needed when `dateString` includes a month or day name and its locale format differs + * from the locale of this machine. + * @returns formatted date string or null if `dateString` is not a valid date + */ + format(dateString: string | null | undefined, dateFormat?: string, locale: string = 'en'): string | null { + if (!dateString) { + return null; + } + + let date: moment.Moment; + + if (!dateFormat) { + // reading date formats other then C2822 or ISO with moment is deprecated + // see https://momentjs.com/docs/#/parsing/string/ + if (this.hasMomentFormat(dateString)) { + // expect C2822 or ISO format + date = moment(dateString); + } else { + // try to read date string with native Date + date = moment(new Date(dateString)); + } + } else { + date = moment(dateString, dateFormat, locale); + } + + // format date (if it is valid) + return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null; + } + + private hasMomentFormat(dateString: string): boolean { + const date = moment(dateString, true); // strict mode + return date.isValid(); + } +} diff --git a/utils/IconList.ts b/utils/IconList.ts new file mode 100644 index 00000000..c98aa75d --- /dev/null +++ b/utils/IconList.ts @@ -0,0 +1,1185 @@ +// credits to phibr0 on discord +export const ICON_LIST = [ + 'activity', + 'airplay', + 'alarm-check', + 'alarm-clock-off', + 'alarm-clock', + 'alarm-minus', + 'alarm-plus', + 'album', + 'alert-circle', + 'alert-octagon', + 'alert-triangle', + 'align-center-horizontal', + 'align-center-vertical', + 'align-center', + 'align-end-horizontal', + 'align-end-vertical', + 'align-horizontal-distribute-center', + 'align-horizontal-distribute-end', + 'align-horizontal-distribute-start', + 'align-horizontal-justify-center', + 'align-horizontal-justify-end', + 'align-horizontal-justify-start', + 'align-horizontal-space-around', + 'align-horizontal-space-between', + 'align-justify', + 'align-left', + 'align-right', + 'align-start-horizontal', + 'align-start-vertical', + 'align-vertical-distribute-center', + 'align-vertical-distribute-end', + 'align-vertical-distribute-start', + 'align-vertical-justify-center', + 'align-vertical-justify-end', + 'align-vertical-justify-start', + 'align-vertical-space-around', + 'align-vertical-space-between', + 'anchor', + 'aperture', + 'archive', + 'arrow-big-down', + 'arrow-big-left', + 'arrow-big-right', + 'arrow-big-up', + 'arrow-down-circle', + 'arrow-down-left', + 'arrow-down-right', + 'arrow-down', + 'arrow-left-circle', + 'arrow-left-right', + 'arrow-left', + 'arrow-right-circle', + 'arrow-right', + 'arrow-up-circle', + 'arrow-up-left', + 'arrow-up-right', + 'arrow-up', + 'asterisk', + 'at-sign', + 'award', + 'axe', + 'banknote', + 'bar-chart-2', + 'bar-chart', + 'baseline', + 'battery-charging', + 'battery-full', + 'battery-low', + 'battery-medium', + 'battery', + 'beaker', + 'bell-minus', + 'bell-off', + 'bell-plus', + 'bell-ring', + 'bell', + 'bike', + 'binary', + 'bitcoin', + 'bluetooth-connected', + 'bluetooth-off', + 'bluetooth-searching', + 'bluetooth', + 'bold', + 'book-open', + 'book', + 'bookmark-minus', + 'bookmark-plus', + 'bookmark', + 'bot', + 'box-select', + 'box', + 'briefcase', + 'brush', + 'bug', + 'building-2', + 'building', + 'bus', + 'calculator', + 'calendar', + 'camera-off', + 'camera', + 'car', + 'carrot', + 'cast', + 'check-circle-2', + 'check-circle', + 'check-square', + 'check', + 'chevron-down', + 'chevron-first', + 'chevron-last', + 'chevron-left', + 'chevron-right', + 'chevron-up', + 'chevrons-down-up', + 'chevrons-down', + 'chevrons-left', + 'chevrons-right', + 'chevrons-up-down', + 'chevrons-up', + 'chrome', + 'circle-slashed', + 'circle', + 'clipboard-check', + 'clipboard-copy', + 'clipboard-list', + 'clipboard-x', + 'clipboard', + 'clock-1', + 'clock-10', + 'clock-11', + 'clock-12', + 'clock-2', + 'clock-3', + 'clock-4', + 'clock-5', + 'clock-6', + 'clock-7', + 'clock-8', + 'clock-9', + 'clock', + 'cloud-drizzle', + 'cloud-fog', + 'cloud-hail', + 'cloud-lightning', + 'cloud-moon', + 'cloud-off', + 'cloud-rain-wind', + 'cloud-rain', + 'cloud-snow', + 'cloud-sun', + 'cloud', + 'cloudy', + 'clover', + 'code-2', + 'code', + 'codepen', + 'codesandbox', + 'coffee', + 'coins', + 'columns', + 'command', + 'compass', + 'contact', + 'contrast', + 'cookie', + 'copy', + 'copyleft', + 'copyright', + 'corner-down-left', + 'corner-down-right', + 'corner-left-down', + 'corner-left-up', + 'corner-right-down', + 'corner-right-up', + 'corner-up-left', + 'corner-up-right', + 'cpu', + 'credit-card', + 'crop', + 'cross', + 'crosshair', + 'crown', + 'currency', + 'database', + 'delete', + 'dice-1', + 'dice-2', + 'dice-3', + 'dice-4', + 'dice-5', + 'dice-6', + 'disc', + 'divide-circle', + 'divide-square', + 'divide', + 'dollar-sign', + 'download-cloud', + 'download', + 'dribbble', + 'droplet', + 'droplets', + 'drumstick', + 'edit-2', + 'edit-3', + 'edit', + 'egg', + 'equal-not', + 'equal', + 'eraser', + 'euro', + 'expand', + 'external-link', + 'eye-off', + 'eye', + 'facebook', + 'fast-forward', + 'feather', + 'figma', + 'file-check-2', + 'file-check', + 'file-code', + 'file-digit', + 'file-input', + 'file-minus-2', + 'file-minus', + 'file-output', + 'file-plus-2', + 'file-plus', + 'file-search', + 'file-text', + 'file-x-2', + 'file-x', + 'file', + 'files', + 'film', + 'filter', + 'flag-off', + 'flag-triangle-left', + 'flag-triangle-right', + 'flag', + 'flame', + 'flashlight-off', + 'flashlight', + 'flask-conical', + 'flask-round', + 'folder-minus', + 'folder-open', + 'folder-plus', + 'folder', + 'form-input', + 'forward', + 'frame', + 'framer', + 'frown', + 'function-square', + 'gamepad-2', + 'gamepad', + 'gauge', + 'gavel', + 'gem', + 'ghost', + 'gift', + 'git-branch-plus', + 'git-branch', + 'git-commit', + 'git-fork', + 'git-merge', + 'git-pull-request', + 'github', + 'gitlab', + 'glasses', + 'globe-2', + 'globe', + 'grab', + 'graduation-cap', + 'grid', + 'grip-horizontal', + 'grip-vertical', + 'hammer', + 'hand-metal', + 'hand', + 'hard-drive', + 'hard-hat', + 'hash', + 'haze', + 'headphones', + 'heart', + 'help-circle', + 'hexagon', + 'highlighter', + 'history', + 'home', + 'image-minus', + 'image-off', + 'image-plus', + 'image', + 'import', + 'inbox', + 'indent', + 'indian-rupee', + 'infinity', + 'info', + 'inspect', + 'instagram', + 'italic', + 'japanese-yen', + 'key', + 'keyboard', + 'landmark', + 'languages', + 'laptop-2', + 'laptop', + 'lasso-select', + 'lasso', + 'layers', + 'layout-dashboard', + 'layout-grid', + 'layout-list', + 'layout-template', + 'layout', + 'library', + 'life-buoy', + 'lightbulb-off', + 'lightbulb', + 'link-2-off', + 'link-2', + 'link', + 'linkedin', + 'list-checks', + 'list-minus', + 'list-ordered', + 'list-plus', + 'list-x', + 'list', + 'loader-2', + 'loader', + 'locate-fixed', + 'locate-off', + 'locate', + 'lock', + 'log-in', + 'log-out', + 'mail', + 'map-pin', + 'map', + 'maximize-2', + 'maximize', + 'megaphone', + 'meh', + 'menu', + 'message-circle', + 'message-square', + 'mic-off', + 'mic', + 'minimize-2', + 'minimize', + 'minus-circle', + 'minus-square', + 'minus', + 'monitor-off', + 'monitor-speaker', + 'monitor', + 'moon', + 'more-horizontal', + 'more-vertical', + 'mountain-snow', + 'mountain', + 'mouse-pointer-2', + 'mouse-pointer-click', + 'mouse-pointer', + 'mouse', + 'move-diagonal-2', + 'move-diagonal', + 'move-horizontal', + 'move-vertical', + 'move', + 'music', + 'navigation-2', + 'navigation', + 'network', + 'octagon', + 'option', + 'outdent', + 'package-check', + 'package-minus', + 'package-plus', + 'package-search', + 'package-x', + 'package', + 'palette', + 'palmtree', + 'paperclip', + 'pause-circle', + 'pause-octagon', + 'pause', + 'pen-tool', + 'pencil', + 'percent', + 'person-standing', + 'phone-call', + 'phone-forwarded', + 'phone-incoming', + 'phone-missed', + 'phone-off', + 'phone-outgoing', + 'phone', + 'pie-chart', + 'piggy-bank', + 'pin', + 'pipette', + 'plane', + 'play-circle', + 'play', + 'plug-zap', + 'plus-circle', + 'plus-square', + 'plus', + 'pocket', + 'podcast', + 'pointer', + 'pound-sterling', + 'power-off', + 'power', + 'printer', + 'qr-code', + 'quote', + 'radio-receiver', + 'radio', + 'redo', + 'refresh-ccw', + 'refresh-cw', + 'regex', + 'repeat-1', + 'repeat', + 'reply-all', + 'reply', + 'rewind', + 'rocket', + 'rocking-chair', + 'rotate-ccw', + 'rotate-cw', + 'rss', + 'ruler', + 'russian-ruble', + 'save', + 'scale', + 'scan-line', + 'scan', + 'scissors', + 'screen-share-off', + 'screen-share', + 'search', + 'send', + 'separator-horizontal', + 'separator-vertical', + 'server-crash', + 'server-off', + 'server', + 'settings-2', + 'settings', + 'share-2', + 'share', + 'sheet', + 'shield-alert', + 'shield-check', + 'shield-close', + 'shield-off', + 'shield', + 'shirt', + 'shopping-bag', + 'shopping-cart', + 'shovel', + 'shrink', + 'shuffle', + 'sidebar-close', + 'sidebar-open', + 'sidebar', + 'sigma', + 'signal-high', + 'signal-low', + 'signal-medium', + 'signal-zero', + 'signal', + 'skip-back', + 'skip-forward', + 'skull', + 'slack', + 'slash', + 'sliders', + 'smartphone-charging', + 'smartphone', + 'smile', + 'snowflake', + 'sort-asc', + 'sort-desc', + 'speaker', + 'sprout', + 'square', + 'star-half', + 'star', + 'stop-circle', + 'stretch-horizontal', + 'stretch-vertical', + 'strikethrough', + 'subscript', + 'sun', + 'sunrise', + 'sunset', + 'superscript', + 'swiss-franc', + 'switch-camera', + 'table', + 'tablet', + 'tag', + 'target', + 'tent', + 'terminal-square', + 'terminal', + 'text-cursor-input', + 'text-cursor', + 'thermometer-snowflake', + 'thermometer-sun', + 'thermometer', + 'thumbs-down', + 'thumbs-up', + 'ticket', + 'timer-off', + 'timer-reset', + 'timer', + 'toggle-left', + 'toggle-right', + 'tornado', + 'trash-2', + 'trash', + 'trello', + 'trending-down', + 'trending-up', + 'triangle', + 'truck', + 'tv-2', + 'tv', + 'twitch', + 'twitter', + 'type', + 'umbrella', + 'underline', + 'undo', + 'unlink-2', + 'unlink', + 'unlock', + 'upload-cloud', + 'upload', + 'user-check', + 'user-minus', + 'user-plus', + 'user-x', + 'user', + 'users', + 'verified', + 'vibrate', + 'video-off', + 'video', + 'view', + 'voicemail', + 'volume-1', + 'volume-2', + 'volume-x', + 'volume', + 'wallet', + 'wand', + 'watch', + 'waves', + 'webcam', + 'wifi-off', + 'wifi', + 'wind', + 'wrap-text', + 'wrench', + 'x-circle', + 'x-octagon', + 'x-square', + 'x', + 'youtube', + 'zap-off', + 'zap', + 'zoom-in', + 'zoom-out', + 'search-large', + 'search', + 'activity', + 'airplay', + 'alarm-check', + 'alarm-clock-off', + 'alarm-clock', + 'alarm-minus', + 'alarm-plus', + 'album', + 'alert-circle', + 'alert-octagon', + 'alert-triangle', + 'align-center-horizontal', + 'align-center-vertical', + 'align-center', + 'align-end-horizontal', + 'align-end-vertical', + 'align-horizontal-distribute-center', + 'align-horizontal-distribute-end', + 'align-horizontal-distribute-start', + 'align-horizontal-justify-center', + 'align-horizontal-justify-end', + 'align-horizontal-justify-start', + 'align-horizontal-space-around', + 'align-horizontal-space-between', + 'align-justify', + 'align-left', + 'align-right', + 'align-start-horizontal', + 'align-start-vertical', + 'align-vertical-distribute-center', + 'align-vertical-distribute-end', + 'align-vertical-distribute-start', + 'align-vertical-justify-center', + 'align-vertical-justify-end', + 'align-vertical-justify-start', + 'align-vertical-space-around', + 'align-vertical-space-between', + 'anchor', + 'aperture', + 'archive', + 'arrow-big-down', + 'arrow-big-left', + 'arrow-big-right', + 'arrow-big-up', + 'arrow-down-circle', + 'arrow-down-left', + 'arrow-down-right', + 'arrow-down', + 'arrow-left-circle', + 'arrow-left-right', + 'arrow-left', + 'arrow-right-circle', + 'arrow-right', + 'arrow-up-circle', + 'arrow-up-left', + 'arrow-up-right', + 'arrow-up', + 'asterisk', + 'at-sign', + 'award', + 'axe', + 'banknote', + 'bar-chart-2', + 'bar-chart', + 'baseline', + 'battery-charging', + 'battery-full', + 'battery-low', + 'battery-medium', + 'battery', + 'beaker', + 'bell-minus', + 'bell-off', + 'bell-plus', + 'bell-ring', + 'bell', + 'bike', + 'binary', + 'bitcoin', + 'bluetooth-connected', + 'bluetooth-off', + 'bluetooth-searching', + 'bluetooth', + 'bold', + 'book-open', + 'book', + 'bookmark-minus', + 'bookmark-plus', + 'bookmark', + 'bot', + 'box-select', + 'box', + 'briefcase', + 'brush', + 'bug', + 'building-2', + 'building', + 'bus', + 'calculator', + 'calendar', + 'camera-off', + 'camera', + 'car', + 'carrot', + 'cast', + 'check-circle-2', + 'check-circle', + 'check-square', + 'check', + 'chevron-down', + 'chevron-first', + 'chevron-last', + 'chevron-left', + 'chevron-right', + 'chevron-up', + 'chevrons-down-up', + 'chevrons-down', + 'chevrons-left', + 'chevrons-right', + 'chevrons-up-down', + 'chevrons-up', + 'chrome', + 'circle-slashed', + 'circle', + 'clipboard-check', + 'clipboard-copy', + 'clipboard-list', + 'clipboard-x', + 'clipboard', + 'clock-1', + 'clock-10', + 'clock-11', + 'clock-12', + 'clock-2', + 'clock-3', + 'clock-4', + 'clock-5', + 'clock-6', + 'clock-7', + 'clock-8', + 'clock-9', + 'lucide-clock', + 'cloud-drizzle', + 'cloud-fog', + 'cloud-hail', + 'cloud-lightning', + 'cloud-moon', + 'cloud-off', + 'cloud-rain-wind', + 'cloud-rain', + 'cloud-snow', + 'cloud-sun', + 'lucide-cloud', + 'cloudy', + 'clover', + 'code-2', + 'code', + 'codepen', + 'codesandbox', + 'coffee', + 'coins', + 'columns', + 'command', + 'compass', + 'contact', + 'contrast', + 'cookie', + 'copy', + 'copyleft', + 'copyright', + 'corner-down-left', + 'corner-down-right', + 'corner-left-down', + 'corner-left-up', + 'corner-right-down', + 'corner-right-up', + 'corner-up-left', + 'corner-up-right', + 'cpu', + 'credit-card', + 'crop', + 'lucide-cross', + 'crosshair', + 'crown', + 'currency', + 'database', + 'delete', + 'dice-1', + 'dice-2', + 'dice-3', + 'dice-4', + 'dice-5', + 'dice-6', + 'disc', + 'divide-circle', + 'divide-square', + 'divide', + 'dollar-sign', + 'download-cloud', + 'download', + 'dribbble', + 'droplet', + 'droplets', + 'drumstick', + 'edit-2', + 'edit-3', + 'edit', + 'egg', + 'equal-not', + 'equal', + 'eraser', + 'euro', + 'expand', + 'external-link', + 'eye-off', + 'eye', + 'facebook', + 'fast-forward', + 'feather', + 'figma', + 'file-check-2', + 'file-check', + 'file-code', + 'file-digit', + 'file-input', + 'file-minus-2', + 'file-minus', + 'file-output', + 'file-plus-2', + 'file-plus', + 'file-search', + 'file-text', + 'file-x-2', + 'file-x', + 'file', + 'files', + 'film', + 'filter', + 'flag-off', + 'flag-triangle-left', + 'flag-triangle-right', + 'flag', + 'flame', + 'flashlight-off', + 'flashlight', + 'flask-conical', + 'flask-round', + 'folder-minus', + 'folder-open', + 'folder-plus', + 'lucide-folder', + 'form-input', + 'forward', + 'frame', + 'framer', + 'frown', + 'function-square', + 'gamepad-2', + 'gamepad', + 'gauge', + 'gavel', + 'gem', + 'ghost', + 'gift', + 'git-branch-plus', + 'git-branch', + 'git-commit', + 'git-fork', + 'git-merge', + 'git-pull-request', + 'github', + 'gitlab', + 'glasses', + 'globe-2', + 'globe', + 'grab', + 'graduation-cap', + 'grid', + 'grip-horizontal', + 'grip-vertical', + 'hammer', + 'hand-metal', + 'hand', + 'hard-drive', + 'hard-hat', + 'hash', + 'haze', + 'headphones', + 'heart', + 'help-circle', + 'hexagon', + 'highlighter', + 'history', + 'home', + 'image-minus', + 'image-off', + 'image-plus', + 'image', + 'import', + 'inbox', + 'indent', + 'indian-rupee', + 'infinity', + 'lucide-info', + 'inspect', + 'instagram', + 'italic', + 'japanese-yen', + 'key', + 'keyboard', + 'landmark', + 'lucide-languages', + 'laptop-2', + 'laptop', + 'lasso-select', + 'lasso', + 'layers', + 'layout-dashboard', + 'layout-grid', + 'layout-list', + 'layout-template', + 'layout', + 'library', + 'life-buoy', + 'lightbulb-off', + 'lightbulb', + 'link-2-off', + 'link-2', + 'lucide-link', + 'linkedin', + 'list-checks', + 'list-minus', + 'list-ordered', + 'list-plus', + 'list-x', + 'list', + 'loader-2', + 'loader', + 'locate-fixed', + 'locate-off', + 'locate', + 'lock', + 'log-in', + 'log-out', + 'mail', + 'map-pin', + 'map', + 'maximize-2', + 'maximize', + 'megaphone', + 'meh', + 'menu', + 'message-circle', + 'message-square', + 'mic-off', + 'mic', + 'minimize-2', + 'minimize', + 'minus-circle', + 'minus-square', + 'minus', + 'monitor-off', + 'monitor-speaker', + 'monitor', + 'moon', + 'more-horizontal', + 'more-vertical', + 'mountain-snow', + 'mountain', + 'mouse-pointer-2', + 'mouse-pointer-click', + 'mouse-pointer', + 'mouse', + 'move-diagonal-2', + 'move-diagonal', + 'move-horizontal', + 'move-vertical', + 'move', + 'music', + 'navigation-2', + 'navigation', + 'network', + 'octagon', + 'option', + 'outdent', + 'package-check', + 'package-minus', + 'package-plus', + 'package-search', + 'package-x', + 'package', + 'palette', + 'palmtree', + 'paperclip', + 'pause-circle', + 'pause-octagon', + 'pause', + 'pen-tool', + 'lucide-pencil', + 'percent', + 'person-standing', + 'phone-call', + 'phone-forwarded', + 'phone-incoming', + 'phone-missed', + 'phone-off', + 'phone-outgoing', + 'phone', + 'pie-chart', + 'piggy-bank', + 'lucide-pin', + 'pipette', + 'plane', + 'play-circle', + 'play', + 'plug-zap', + 'plus-circle', + 'plus-square', + 'plus', + 'pocket', + 'podcast', + 'pointer', + 'pound-sterling', + 'power-off', + 'power', + 'printer', + 'qr-code', + 'quote', + 'radio-receiver', + 'radio', + 'redo', + 'refresh-ccw', + 'refresh-cw', + 'regex', + 'repeat-1', + 'repeat', + 'reply-all', + 'reply', + 'rewind', + 'rocket', + 'rocking-chair', + 'rotate-ccw', + 'rotate-cw', + 'rss', + 'ruler', + 'russian-ruble', + 'save', + 'scale', + 'scan-line', + 'scan', + 'scissors', + 'screen-share-off', + 'screen-share', + 'lucide-search', + 'send', + 'separator-horizontal', + 'separator-vertical', + 'server-crash', + 'server-off', + 'server', + 'settings-2', + 'settings', + 'share-2', + 'share', + 'sheet', + 'shield-alert', + 'shield-check', + 'shield-close', + 'shield-off', + 'shield', + 'shirt', + 'shopping-bag', + 'shopping-cart', + 'shovel', + 'shrink', + 'shuffle', + 'sidebar-close', + 'sidebar-open', + 'sidebar', + 'sigma', + 'signal-high', + 'signal-low', + 'signal-medium', + 'signal-zero', + 'signal', + 'skip-back', + 'skip-forward', + 'skull', + 'slack', + 'slash', + 'sliders', + 'smartphone-charging', + 'smartphone', + 'smile', + 'snowflake', + 'sort-asc', + 'sort-desc', + 'speaker', + 'sprout', + 'square', + 'star-half', + 'lucide-star', + 'stop-circle', + 'stretch-horizontal', + 'stretch-vertical', + 'strikethrough', + 'subscript', + 'sun', + 'sunrise', + 'sunset', + 'superscript', + 'swiss-franc', + 'switch-camera', + 'table', + 'tablet', + 'tag', + 'target', + 'tent', + 'terminal-square', + 'terminal', + 'text-cursor-input', + 'text-cursor', + 'thermometer-snowflake', + 'thermometer-sun', + 'thermometer', + 'thumbs-down', + 'thumbs-up', + 'ticket', + 'timer-off', + 'timer-reset', + 'timer', + 'toggle-left', + 'toggle-right', + 'tornado', + 'trash-2', + 'lucide-trash', + 'trello', + 'trending-down', + 'trending-up', + 'triangle', + 'truck', + 'tv-2', + 'tv', + 'twitch', + 'twitter', + 'type', + 'umbrella', + 'underline', + 'undo', + 'unlink-2', + 'unlink', + 'unlock', + 'upload-cloud', + 'upload', + 'user-check', + 'user-minus', + 'user-plus', + 'user-x', + 'user', + 'users', + 'verified', + 'vibrate', + 'video-off', + 'video', + 'view', + 'voicemail', + 'volume-1', + 'volume-2', + 'volume-x', + 'volume', + 'wallet', + 'wand', + 'watch', + 'waves', + 'webcam', + 'wifi-off', + 'wifi', + 'wind', + 'wrap-text', + 'wrench', + 'x-circle', + 'x-octagon', + 'x-square', + 'x', + 'youtube', + 'zap-off', + 'zap', + 'zoom-in', + 'zoom-out', + 'search-large', + 'lucide-search', +]; diff --git a/utils/IllegalFilenameCharactersList.ts b/utils/IllegalFilenameCharactersList.ts new file mode 100644 index 00000000..23332276 --- /dev/null +++ b/utils/IllegalFilenameCharactersList.ts @@ -0,0 +1,14 @@ +// Illegal characters in the form `[illegal_character, replacement][]` +export const ILLEGAL_FILENAME_CHARACTERS = [ + ['/', '-'], + ['\\', '-'], + ['<', ''], + ['>', ''], + [':', ' - '], + ['"', "'"], + ['|', ' - '], + ['?', ''], + ['*', ''], + ['^', ''], + ['#', ''], +]; diff --git a/utils/MediaType.ts b/utils/MediaType.ts new file mode 100644 index 00000000..a9dfb548 --- /dev/null +++ b/utils/MediaType.ts @@ -0,0 +1,13 @@ +export enum MediaType { + Artist = 'artist', + BoardGame = 'boardgame', + Book = 'book', + ComicManga = 'comicManga', + Game = 'game', + Movie = 'movie', + MusicRelease = 'musicRelease', + Season = 'season', + Series = 'series', + Song = 'song', + Wiki = 'wiki', +} diff --git a/utils/MediaTypeManager.ts b/utils/MediaTypeManager.ts new file mode 100644 index 00000000..3b51f8b3 --- /dev/null +++ b/utils/MediaTypeManager.ts @@ -0,0 +1,179 @@ +import type { App, TFile } from 'obsidian'; +import { TFolder } from 'obsidian'; +import { ArtistModel } from '../models/ArtistModel'; +import { BoardGameModel } from '../models/BoardGameModel'; +import { BookModel } from '../models/BookModel'; +import { ComicMangaModel } from '../models/ComicMangaModel'; +import { GameModel } from '../models/GameModel'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import { MovieModel } from '../models/MovieModel'; +import { MusicReleaseModel } from '../models/MusicReleaseModel'; +import { SeasonModel } from '../models/SeasonModel'; +import { SeriesModel } from '../models/SeriesModel'; +import { SongModel } from '../models/SongModel'; +import { WikiModel } from '../models/WikiModel'; +import type { MediaDbPluginSettings } from '../settings/Settings'; +import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; +import { MediaType } from './MediaType'; +import { replaceTags } from './Utils'; + +// All media types in alphabetical order +export const MEDIA_TYPES: MediaType[] = [ + MediaType.Artist, + MediaType.BoardGame, + MediaType.Book, + MediaType.ComicManga, + MediaType.Game, + MediaType.Movie, + MediaType.MusicRelease, + MediaType.Series, + MediaType.Season, + MediaType.Song, + MediaType.Wiki, +]; + +export class MediaTypeManager { + mediaFileNameTemplateMap: Map; + mediaTemplateMap: Map; + mediaFolderMap: Map; + + constructor() { + this.mediaFileNameTemplateMap = new Map(); + this.mediaTemplateMap = new Map(); + this.mediaFolderMap = new Map(); + } + + updateTemplates(settings: MediaDbPluginSettings): void { + this.mediaFileNameTemplateMap = new Map(); + this.mediaFileNameTemplateMap.set(MediaType.Artist, settings.artistFileNameTemplate); + 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); + this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.BoardGame, settings.boardgameFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.Book, settings.bookFileNameTemplate); + this.mediaFileNameTemplateMap.set(MediaType.Song, settings.songFileNameTemplate); + + this.mediaTemplateMap = new Map(); + this.mediaTemplateMap.set(MediaType.Artist, settings.artistTemplate); + 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); + this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); + this.mediaTemplateMap.set(MediaType.BoardGame, settings.boardgameTemplate); + this.mediaTemplateMap.set(MediaType.Book, settings.bookTemplate); + this.mediaTemplateMap.set(MediaType.Song, settings.songTemplate); + } + + updateFolders(settings: MediaDbPluginSettings): void { + this.mediaFolderMap = new Map(); + this.mediaFolderMap.set(MediaType.Artist, settings.artistFolder); + 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); + this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); + this.mediaFolderMap.set(MediaType.BoardGame, settings.boardgameFolder); + this.mediaFolderMap.set(MediaType.Book, settings.bookFolder); + this.mediaFolderMap.set(MediaType.Song, settings.songFolder); + } + + getFileName(mediaTypeModel: MediaTypeModel): string { + // Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur + const fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true); + return this.cleanFileName(fileName); + } + + cleanFileName(fileName: string): string { + const cleanedFileName = ILLEGAL_FILENAME_CHARACTERS.reduce((str, char) => str.replaceAll(char[0], char[1]), fileName); + // Remove all duplicate whitespace in the file name + return cleanedFileName.replaceAll(/ +/g, ' '); + } + + async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise { + const templateFilePath = this.mediaTemplateMap.get(mediaTypeModel.getMediaType()); + + if (!templateFilePath) { + return ''; + } + + let templateFile = app.vault.getAbstractFileByPath(templateFilePath) ?? undefined; + + // WARNING: This was previously selected by filename, but that could lead to collisions and unwanted effects. + // This now falls back to the previous method if no file is found + if (!templateFile || templateFile instanceof TFolder) { + templateFile = app.vault + .getFiles() + .filter((f: TFile) => f.name === templateFilePath) + .first(); + + if (!templateFile) { + return ''; + } + } + + const template = await app.vault.cachedRead(templateFile as TFile); + // console.log(template); + return replaceTags(template, mediaTypeModel); + } + + async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise { + let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); + + folderPath ??= `/`; + // console.log(folderPath); + + if (!(await app.vault.adapter.exists(folderPath))) { + await app.vault.createFolder(folderPath); + } + const folder = app.vault.getAbstractFileByPath(folderPath); + + if (!(folder instanceof TFolder)) { + throw Error(`Expected ${folder?.path} to be instance of TFolder`); + } + + return folder; + } + + /** + * Takes an object and a MediaType and turns the object into an instance of a MediaTypeModel corresponding to the MediaType passed in. + * + * @param obj + * @param mediaType + */ + createMediaTypeModelFromMediaType(obj: object, mediaType: MediaType): MediaTypeModel { + if (mediaType === MediaType.Movie) { + 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) { + return new GameModel(obj); + } else if (mediaType === MediaType.Wiki) { + return new WikiModel(obj); + } else if (mediaType === MediaType.MusicRelease) { + return new MusicReleaseModel(obj); + } else if (mediaType === MediaType.BoardGame) { + return new BoardGameModel(obj); + } else if (mediaType === MediaType.Book) { + return new BookModel(obj); + } else if (mediaType === MediaType.Artist) { + return new ArtistModel(obj); + } else if (mediaType === MediaType.Song) { + return new SongModel(obj); + } + + throw new Error(`Unknown media type: ${mediaType}`); + } +} diff --git a/utils/ModalHelper.ts b/utils/ModalHelper.ts new file mode 100644 index 00000000..7efba29f --- /dev/null +++ b/utils/ModalHelper.ts @@ -0,0 +1,537 @@ +import { Notice } from 'obsidian'; +import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal'; +import type MediaDbPlugin from '../main'; +import { MediaDbAdvancedSearchModal } from '../modals/MediaDbAdvancedSearchModal'; +import { MediaDbIdSearchModal } from '../modals/MediaDbIdSearchModal'; +import { MediaDbSearchModal } from '../modals/MediaDbSearchModal'; +import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import type { MediaType } from './MediaType'; + +export enum ModalResultCode { + SUCCESS = 'SUCCESS', + SKIP = 'SKIP', + CLOSE = 'CLOSE', + ERROR = 'ERROR', +} + +type ModalResult = + | { + code: ModalResultCode.CLOSE; + } + | { + code: ModalResultCode.ERROR; + error: Error; + } + | { + code: ModalResultCode.SUCCESS; + data: T; + }; + +type SkippableModalResult = + | ModalResult + | { + code: ModalResultCode.SKIP; + }; + +/** + * Object containing the data {@link ModalHelper.createSearchModal} returns. + * On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}. + * On {@link ModalResultCode.ERROR} this contains a reference to that error. + */ +export type SearchModalResult = ModalResult; + +/** + * Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns. + * On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}. + * On {@link ModalResultCode.ERROR} this contains a reference to that error. + */ +export type AdvancedSearchModalResult = ModalResult; + +/** + * Object containing the data {@link ModalHelper.createIdSearchModal} returns. + * On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}. + * On {@link ModalResultCode.ERROR} this contains a reference to that error. + */ +export type IdSearchModalResult = ModalResult; + +/** + * Object containing the data {@link ModalHelper.createSelectModal} returns. + * On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}. + * On {@link ModalResultCode.ERROR} this contains a reference to that error. + */ +export type SelectModalResult = SkippableModalResult; + +/** + * Object containing the data {@link ModalHelper.createPreviewModal} returns. + * On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}. + * On {@link ModalResultCode.ERROR} this contains a reference to that error. + */ +export type PreviewModalResult = ModalResult; + +/** + * The data the search modal returns. + * - query: the query string + * - types: the selected APIs + */ +export interface SearchModalData { + query: string; + types: MediaType[]; +} + +/** + * The data the advanced search modal returns. + * - query: the query string + * - apis: the selected APIs + */ +export interface AdvancedSearchModalData { + query: string; + apis: string[]; +} + +/** + * The data the id search modal returns. + * - query: the query string + * - apis: the selected APIs + */ +export interface IdSearchModalData { + query: string; + api: string; +} + +/** + * The data the select modal returns. + * - selected: the selected items + */ +export interface SelectModalData { + selected: MediaTypeModel[]; +} + +/** + * The data the preview modal returns. + * - confirmed: whether the selected element has been confirmed + */ +export interface PreviewModalData { + confirmed: boolean; +} + +/** + * Options for the search modal. + * - modalTitle: the title of the modal + * - preselectedTypes: a list of preselected Types + * - prefilledSearchString: prefilled query + */ +export interface SearchModalOptions { + modalTitle?: string; + preselectedTypes?: MediaType[]; + prefilledSearchString?: string; +} + +/** + * Options for the advanced search modal. + * - modalTitle: the title of the modal + * - preselectedAPIs: a list of preselected APIs + * - prefilledSearchString: prefilled query + */ +export interface AdvancedSearchModalOptions { + modalTitle?: string; + preselectedAPIs?: string[]; + prefilledSearchString?: string; +} + +/** + * Options for the id search modal. + * - modalTitle: the title of the modal + * - preselectedAPIs: a list of preselected APIs + * - prefilledSearchString: prefilled query + */ +export interface IdSearchModalOptions { + modalTitle?: string; + preselectedAPI?: string; + prefilledSearchString?: string; +} + +/** + * Options for the select modal. + * - modalTitle: the title of the modal + * - elements: the elements the user can select from + * - multiSelect: whether to allow multiselect + * - skipButton: whether to add a skip button to the modal + */ +export interface SelectModalOptions { + elements?: MediaTypeModel[]; + multiSelect?: boolean; + modalTitle?: string; + skipButton?: boolean; + description?: string; // Add this + submitButtonText?: string; // Add this too +} +/** + * Options for the preview modal. + * - modalTitle: the title of the modal + * - elements: the elements to preview + */ +export interface PreviewModalOptions { + modalTitle?: string; + elements?: MediaTypeModel[]; +} + +export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { + modalTitle: 'Media DB Search', + preselectedTypes: [], + prefilledSearchString: '', +}; + +export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { + modalTitle: 'Media DB Advanced Search', + preselectedAPIs: [], + prefilledSearchString: '', +}; + +export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { + modalTitle: 'Media DB Id Search', + preselectedAPI: undefined, + prefilledSearchString: '', +}; + +export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { + modalTitle: 'Media DB Search Results', + elements: [], + multiSelect: true, + skipButton: false, +}; + +export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { + modalTitle: 'Media DB Preview', + elements: [], +}; + +export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { + elements: [], + multiSelect: true, + modalTitle: '', + skipButton: false, + description: 'Select one or multiple search results.', + submitButtonText: 'Ok', +}; + +/** + * A class providing multiple usefull functions for dealing with the plugins modals. + */ +export class ModalHelper { + plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + /** + * Creates an {@link MediaDbSearchModal}, then sets callbacks and awaits them, + * returning either the user input once submitted or nothing once closed. + * The modal needs ot be manually closed by calling `close()` on the modal reference. + * + * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} + * @returns the user input or nothing and a reference to the modal. + */ + async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult; searchModal: MediaDbSearchModal }> { + const modal = new MediaDbSearchModal(this.plugin, searchModalOptions); + const res: SearchModalResult = await new Promise(resolve => { + modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); + modal.setCloseCb(err => { + if (err) { + resolve({ code: ModalResultCode.ERROR, error: err }); + } + resolve({ code: ModalResultCode.CLOSE }); + }); + + modal.open(); + }); + return { searchModalResult: res, searchModal: modal }; + } + + /** + * Opens an {@link MediaDbSearchModal} and awaits its result, + * then executes the `submitCallback` returning the callbacks result and closing the modal. + * + * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} + * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed + * @returns the user input or nothing and a reference to the modal. + */ + async openSearchModal( + searchModalOptions: SearchModalOptions, + submitCallback: (searchModalData: SearchModalData) => Promise, + ): Promise { + const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions); + console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`); + + if (searchModalResult.code === ModalResultCode.ERROR) { + // there was an error in the modal itself + console.warn(searchModalResult.error); + new Notice(searchModalResult.error.toString()); + searchModal.close(); + return undefined; + } + + if (searchModalResult.code === ModalResultCode.CLOSE) { + // modal is already being closed + return undefined; + } + + try { + const callbackRes: MediaTypeModel[] = await submitCallback(searchModalResult.data); + searchModal.close(); + return callbackRes; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + searchModal.close(); + return undefined; + } + } + + /** + * Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them, + * returning either the user input once submitted or nothing once closed. + * The modal needs ot be manually closed by calling `close()` on the modal reference. + * + * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} + * @returns the user input or nothing and a reference to the modal. + */ + async createAdvancedSearchModal( + advancedSearchModalOptions: AdvancedSearchModalOptions, + ): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult; advancedSearchModal: MediaDbAdvancedSearchModal }> { + const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions); + const res: AdvancedSearchModalResult = await new Promise(resolve => { + modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); + modal.setCloseCb(err => { + if (err) { + resolve({ code: ModalResultCode.ERROR, error: err }); + } + resolve({ code: ModalResultCode.CLOSE }); + }); + + modal.open(); + }); + return { advancedSearchModalResult: res, advancedSearchModal: modal }; + } + + /** + * Opens an {@link MediaDbAdvancedSearchModal} and awaits its result, + * then executes the `submitCallback` returning the callbacks result and closing the modal. + * + * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} + * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed + * @returns the user input or nothing and a reference to the modal. + */ + async openAdvancedSearchModal( + advancedSearchModalOptions: AdvancedSearchModalOptions, + submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise, + ): Promise { + const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions); + console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`); + + if (advancedSearchModalResult.code === ModalResultCode.ERROR) { + // there was an error in the modal itself + console.warn(advancedSearchModalResult.error); + new Notice(advancedSearchModalResult.error.toString()); + advancedSearchModal.close(); + return undefined; + } + + if (advancedSearchModalResult.code === ModalResultCode.CLOSE) { + // modal is already being closed + return undefined; + } + + try { + const callbackRes: MediaTypeModel[] = await submitCallback(advancedSearchModalResult.data); + advancedSearchModal.close(); + return callbackRes; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + advancedSearchModal.close(); + return undefined; + } + } + + /** + * Creates an {@link MediaDbIdSearchModal}, then sets callbacks and awaits them, + * returning either the user input once submitted or nothing once closed. + * The modal needs ot be manually closed by calling `close()` on the modal reference. + * + * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} + * @returns the user input or nothing and a reference to the modal. + */ + async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult; idSearchModal: MediaDbIdSearchModal }> { + const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions); + const res: IdSearchModalResult = await new Promise(resolve => { + modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); + modal.setCloseCb(err => { + if (err) { + resolve({ code: ModalResultCode.ERROR, error: err }); + } + resolve({ code: ModalResultCode.CLOSE }); + }); + + modal.open(); + }); + return { idSearchModalResult: res, idSearchModal: modal }; + } + + /** + * Opens an {@link MediaDbIdSearchModal} and awaits its result, + * then executes the `submitCallback` returning the callbacks result and closing the modal. + * + * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} + * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed + * @returns the user input or nothing and a reference to the modal. + */ + async openIdSearchModal( + idSearchModalOptions: IdSearchModalOptions, + submitCallback: (idSearchModalData: IdSearchModalData) => Promise, + ): Promise { + const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions); + console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`); + + if (idSearchModalResult.code === ModalResultCode.ERROR) { + // there was an error in the modal itself + console.warn(idSearchModalResult.error); + new Notice(idSearchModalResult.error.toString()); + idSearchModal.close(); + return undefined; + } + + if (idSearchModalResult.code === ModalResultCode.CLOSE) { + // modal is already being closed + return undefined; + } + + try { + const callbackRes = await submitCallback(idSearchModalResult.data); + idSearchModal.close(); + return callbackRes; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + idSearchModal.close(); + return undefined; + } + } + + /** + * Creates an {@link MediaDbSearchResultModal}, then sets callbacks and awaits them, + * returning either the user input once submitted or nothing once closed. + * The modal needs ot be manually closed by calling `close()` on the modal reference. + * + * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} + * @returns the user input or nothing and a reference to the modal. + */ + async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult; selectModal: MediaDbSearchResultModal }> { + const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions); + const res: SelectModalResult = await new Promise(resolve => { + modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); + modal.setSkipCallback(() => resolve({ code: ModalResultCode.SKIP })); + modal.setCloseCb(err => { + if (err) { + resolve({ code: ModalResultCode.ERROR, error: err }); + } + resolve({ code: ModalResultCode.CLOSE }); + }); + + modal.open(); + }); + return { selectModalResult: res, selectModal: modal }; + } + + /** + * Opens an {@link MediaDbSearchResultModal} and awaits its result, + * then executes the `submitCallback` returning the callbacks result and closing the modal. + * + * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} + * @param submitCallback the callback that gets executed after the modal has been submitted, but before it has been closed + * @returns the user input or nothing and a reference to the modal. + */ + async openSelectModal( + selectModalOptions: SelectModalOptions, + submitCallback: (selectModalData: SelectModalData) => Promise, + ): Promise { + const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions); + console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`); + + if (selectModalResult.code === ModalResultCode.ERROR) { + // there was an error in the modal itself + console.warn(selectModalResult.error); + new Notice(selectModalResult.error.toString()); + selectModal.close(); + return undefined; + } + + if (selectModalResult.code === ModalResultCode.CLOSE) { + // modal is already being closed + return undefined; + } + + if (selectModalResult.code === ModalResultCode.SKIP) { + // selection was skipped + return undefined; + } + + try { + const callbackRes: MediaTypeModel[] = await submitCallback(selectModalResult.data); + selectModal.close(); + return callbackRes; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + selectModal.close(); + return; + } + } + + async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult; previewModal: MediaDbPreviewModal }> { + //todo: handle attachFile for existing files + const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions); + const res: PreviewModalResult = await new Promise(resolve => { + modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); + modal.setCloseCb(err => { + if (err) { + resolve({ code: ModalResultCode.ERROR, error: err }); + } + resolve({ code: ModalResultCode.CLOSE }); + }); + + modal.open(); + }); + return { previewModalResult: res, previewModal: modal }; + } + + async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise): Promise { + const { previewModalResult, previewModal } = await this.createPreviewModal(previewModalOptions); + console.debug(`MDB | previewModal closed with code ${previewModalResult.code}`); + + if (previewModalResult.code === ModalResultCode.ERROR) { + // there was an error in the modal itself + console.warn(previewModalResult.error); + new Notice(previewModalResult.error.toString()); + previewModal.close(); + return false; + } + + if (previewModalResult.code === ModalResultCode.CLOSE) { + // modal is already being closed + return false; + } + + try { + const callbackRes: boolean = await submitCallback(previewModalResult.data); + previewModal.close(); + return callbackRes; + } catch (e) { + console.warn(e); + new Notice(`${e}`); + previewModal.close(); + return true; + } + } +} diff --git a/utils/Utils.ts b/utils/Utils.ts new file mode 100644 index 00000000..b4a16b99 --- /dev/null +++ b/utils/Utils.ts @@ -0,0 +1,387 @@ +import { iso6392 } from 'iso-639-2'; +import type { TFile, TFolder, App } from 'obsidian'; +import { requestUrl } from 'obsidian'; +import type { MediaTypeModel } from '../models/MediaTypeModel'; +import { MediaType } from './MediaType'; + +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.5.2'; +export const debug: boolean = true; + +export function wrapAround(value: number, size: number): number { + if (size <= 0) { + throw Error('size may not be zero or negative'); + } + return mod(value, size); +} + +export function containsOnlyLettersAndUnderscores(str: string): boolean { + return /^[\p{Letter}\p{M}_]+$/u.test(str); +} + +export function replaceIllegalFileNameCharactersInString(string: string): string { + return string.replace(/[\\/:"*?<>|]/g, '-'); +} + +export function replaceTags(template: string, mediaTypeModel: MediaTypeModel, ignoreUndefined: boolean = false): string { + return template.replace(new RegExp('{{.*?}}', 'g'), (match: string) => replaceTag(match, mediaTypeModel, ignoreUndefined)); +} + +function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefined: boolean): string { + let tag = match; + tag = tag.substring(2); + tag = tag.substring(0, tag.length - 2); + tag = tag.trim(); + + const parts = tag.split(':'); + if (parts.length === 1) { + const path = parts[0].split('.'); + + const obj = traverseMetaData(path, mediaTypeModel); + + if (obj === undefined) { + return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; + } + // year: 0 means "unknown" — return empty string so filename templates stay clean (e.g. "Title ()") + if (path[path.length - 1] === 'year' && obj === 0) { + return ''; + } + + // eslint-disable-next-line @typescript-eslint/no-base-to-string + return obj?.toString() ?? 'null'; + } else if (parts.length === 2) { + const operator = parts[0]; + + const path = parts[1].split('.'); + + const obj = traverseMetaData(path, mediaTypeModel); + + if (obj === undefined) { + return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; + } + + if (operator === 'LIST') { + if (!Array.isArray(obj)) { + return '{{ INVALID TEMPLATE TAG - operator LIST is only applicable on an array }}'; + } + + return obj.map((e: unknown) => `- ${e}`).join('\n'); + } else if (operator === 'ENUM') { + if (!Array.isArray(obj)) { + return '{{ INVALID TEMPLATE TAG - operator ENUM is only applicable on an array }}'; + } + return obj.join(', '); + } else if (operator === 'FIRST') { + if (!Array.isArray(obj)) { + return '{{ INVALID TEMPLATE TAG - operator FIRST is only applicable on an array }}'; + } + + const first = obj[0] as unknown; + return first?.toString() ?? 'null'; + } else if (operator === 'LAST') { + if (!Array.isArray(obj)) { + return '{{ INVALID TEMPLATE TAG - operator LAST is only applicable on an array }}'; + } + + const last = obj[obj.length - 1] as unknown; + return last?.toString() ?? 'null'; + } + + return `{{ INVALID TEMPLATE TAG - unknown operator ${operator} }}`; + } + + return '{{ INVALID TEMPLATE TAG }}'; +} + +function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): unknown { + let o: unknown = mediaTypeModel; + + for (const part of path) { + if (o !== undefined) { + o = (o as Record)[part]; + } + } + + return o; +} + +export function markdownTable(content: string[][]): string { + const rows = content.length; + if (rows === 0) { + return ''; + } + + const columns = content[0].length; + if (columns === 0) { + return ''; + } + for (const row of content) { + if (row.length !== columns) { + return ''; + } + } + + const longestStringInColumns: number[] = []; + + for (let i = 0; i < columns; i++) { + let longestStringInColumn = 0; + for (const row of content) { + if (row[i].length > longestStringInColumn) { + longestStringInColumn = row[i].length; + } + } + + longestStringInColumns.push(longestStringInColumn); + } + + let table = ''; + + for (let i = 0; i < rows; i++) { + table += '|'; + for (let j = 0; j < columns; j++) { + let element = content[i][j]; + element += ' '.repeat(longestStringInColumns[j] - element.length); + table += ' ' + element + ' |'; + } + table += '\n'; + if (i === 0) { + table += '|'; + for (let j = 0; j < columns; j++) { + table += ' ' + '-'.repeat(longestStringInColumns[j]) + ' |'; + } + table += '\n'; + } + } + + return table; +} + +export function fragWithHTML(html: string): DocumentFragment { + return createFragment(frag => (frag.createDiv().innerHTML = html)); +} + +export function dateToString(date: Date): string { + return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; +} + +export function timeToString(time: Date): string { + return `${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`; +} + +export function dateTimeToString(dateTime: Date): string { + return `${dateToString(dateTime)} ${timeToString(dateTime)}`; +} + +// js can't even implement modulo correctly... +export function mod(n: number, m: number): number { + return ((n % m) + m) % m; +} + +export function capitalizeFirstLetter(string: string): string { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +export class PropertyMappingValidationError extends Error { + constructor(message: string) { + super(message); + } +} + +export class PropertyMappingNameConflictError extends Error { + constructor(message: string) { + super(message); + } +} + +/** + * - attachTemplate: whether to attach the template (DEFAULT: false) + * - attachFie: a file to attach (DEFAULT: undefined) + * - openNote: whether to open the note after creation (DEFAULT: false) + * - folder: folder to put the note in + */ +export interface CreateNoteOptions { + attachTemplate?: boolean; + attachFile?: TFile; + openNote?: boolean; + folder?: TFolder; + overwrite?: boolean; +} + +/** Runtime in whole minutes (TMDB/OMDb/MAL). 0 when unknown. Parses legacy string frontmatter (e.g. "136 min", "2 hr 5 min"). */ +export function coerceMovieDurationMinutes(value: unknown): number { + if (value === undefined || value === null) { + return 0; + } + if (typeof value === 'number') { + const n = Math.trunc(value); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + if (typeof value === 'string') { + const t = value.trim(); + if (t === '' || t.toLowerCase() === 'unknown' || t.toUpperCase() === 'N/A' || t === 'TBA') { + return 0; + } + let total = 0; + const hours = t.match(/(\d+)\s*(?:hours?|hrs?)\b/i) ?? t.match(/(\d+)\s*h\b/i); + const mins = t.match(/(\d+)\s*(?:minutes?|mins?)\b/i) ?? t.match(/(\d+)\s*min\b/i); + if (hours) { + total += parseInt(hours[1], 10) * 60; + } + if (mins) { + total += parseInt(mins[1], 10); + } + if (total > 0) { + return total; + } + const n = parseInt(t, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + return 0; +} + +/** Normalizes release year for metadata: integer, 0 when unknown or non-numeric. */ +export function coerceYear(value: unknown): number { + if (value === undefined || value === null) return 0; + if (typeof value === 'number') { + const n = Math.trunc(value); + return Number.isFinite(n) ? n : 0; + } + if (typeof value === 'string') { + const t = value.trim(); + if (t === '' || t.toLowerCase() === 'unknown' || t === 'TBA' || t.toUpperCase() === 'N/A') { + return 0; + } + const n = parseInt(t, 10); + return Number.isFinite(n) ? n : 0; + } + return 0; +} + +export function migrateObject(object: T, oldData: Record, defaultData: T): void { + for (const key in object) { + const has = Object.hasOwn(oldData, key) && oldData[key] !== undefined && oldData[key] !== null; + if (!has) { + object[key] = defaultData[key]; + continue; + } + const raw = oldData[key]; + if (key === 'year') { + (object as Record)[key] = coerceYear(raw); + continue; + } + object[key] = raw as T[typeof key]; + } +} + +export function unCamelCase(str: string): string { + return ( + str + // insert a space between lower & upper + .replace(/([a-z])([A-Z])/g, '$1 $2') + // space before last upper in a sequence followed by lower + .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') + // uppercase the first character + .replace(/^./, function (str) { + return str.toUpperCase(); + }) + ); +} + +/** User-facing label for a media type (e.g. MusicRelease → Album). */ +export function mediaTypeDisplayName(mediaType: MediaType): string { + if (mediaType === MediaType.MusicRelease) { + return 'Album'; + } + return unCamelCase(mediaType); +} + +/* eslint-disable */ + +export function hasTemplaterPlugin(app: App): boolean { + const templater = (app as any).plugins.plugins['templater-obsidian']; + + return !!templater; +} + +// Copied from https://github.com/anpigon/obsidian-book-search-plugin +// Licensed under the MIT license. Copyright (c) 2020 Jake Runzer +export async function useTemplaterPluginInFile(app: App, file: TFile): Promise { + const templater = (app as any).plugins.plugins['templater-obsidian']; + if (templater && !templater?.settings.trigger_on_file_creation) { + await templater.templater.overwrite_file_commands(file); + } +} + +/* eslint-enable */ + +/** Whole USD amounts as used by TMDB (empty when unknown or non-positive). */ +export function formatUsdWholeDollars(amount: number): string { + if (!Number.isFinite(amount) || amount <= 0) { + return ''; + } + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount); +} + +export type ModelToData = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + [K in keyof T as T[K] extends Function ? never : K]?: T[K] | null; +}; + +// Checks if a given URL points to an existing image (status 200), or returns false for 404/other errors. + +export async function imageUrlExists(url: string): Promise { + try { + // @ts-ignore + const response = await requestUrl({ + url, + method: 'HEAD', + throw: false, + }); + return response.status === 200; + } catch { + return false; + } +} + +export function isTruthy(value: T): value is Exclude { + return Boolean(value); +} + +/** + * Wraps Obsidians `requestUrl` in a fetch like API. + */ +export async function obsidianFetch(input: Request): Promise { + const obs_headers: Record = {}; + input.headers.forEach((header, value) => { + obs_headers[header] = value; + }); + + const res = await requestUrl({ + url: input.url, + method: input.method, + headers: obs_headers, + throw: false, // Do not throw on error, handle it manually + }); + + const responseHeaders: Headers = new Headers(); + for (const [key, value] of Object.entries(res.headers)) { + responseHeaders.append(key, value); + } + + return { + ok: res.status >= 200 && res.status < 300, + status: res.status, + headers: responseHeaders, + // eslint-disable-next-line + json: async () => res.json, + text: async () => res.text, + } as Response; +} + +export function getLanguageName(code: string): string | null { + const language = iso6392.find(lang => lang.iso6392B === code || lang.iso6392T === code); + + return language?.name ?? null; +} diff --git a/utils/normalizeTitleForAlias.ts b/utils/normalizeTitleForAlias.ts new file mode 100644 index 00000000..fd229be0 --- /dev/null +++ b/utils/normalizeTitleForAlias.ts @@ -0,0 +1,52 @@ +/** + * ASCII-style form of a title for use as an Obsidian `aliases` entry (e.g. Likbør → Likbor). + * Returns null when the title should not get an extra alias (unchanged after normalization). + * + * NFKD + stripping marks handles most Latin letters; pairs below cover letters that do not + * decompose usefully (ligatures, stroke letters, eth/thorn, eng, Turkish dotless i, …). + */ +const ASCII_ALIAS_FOLDS: readonly [string, string][] = [ + ['æ', 'ae'], + ['Æ', 'Ae'], + ['œ', 'oe'], + ['Œ', 'Oe'], + ['ø', 'o'], + ['Ø', 'O'], + ['ß', 'ss'], + ['ẞ', 'SS'], + ['ð', 'd'], + ['Ð', 'D'], + ['þ', 'th'], + ['Þ', 'Th'], + ['đ', 'd'], + ['Đ', 'D'], + ['ı', 'i'], + ['ħ', 'h'], + ['Ħ', 'H'], + ['ŋ', 'ng'], + ['Ŋ', 'Ng'], + ['Ł', 'L'], + ['ł', 'l'], + ['Ŀ', 'L'], + ['ŀ', 'l'], + ['ĸ', 'k'], + ['ʼn', "'n"], + ['ƿ', 'w'], +]; + +export function normalizeTitleForAsciiAlias(title: string): string | null { + const trimmed = title.trim(); + if (!trimmed) { + return null; + } + + let s = trimmed.normalize('NFKD').replace(/\p{M}/gu, ''); + for (const [from, to] of ASCII_ALIAS_FOLDS) { + s = s.replaceAll(from, to); + } + + if (s === trimmed) { + return null; + } + return s; +} diff --git a/utils/noteTypeSettings.ts b/utils/noteTypeSettings.ts new file mode 100644 index 00000000..31b51d70 --- /dev/null +++ b/utils/noteTypeSettings.ts @@ -0,0 +1,63 @@ +import type { MediaDbPluginSettings } from '../settings/Settings'; +import { MEDIA_TYPES } from './MediaTypeManager'; +import { MediaType } from './MediaType'; + +const MEDIA_TYPE_TO_NOTE_TYPE_KEY: Record = { + [MediaType.Artist]: 'artistNoteType', + [MediaType.BoardGame]: 'boardgameNoteType', + [MediaType.Book]: 'bookNoteType', + [MediaType.ComicManga]: 'mangaNoteType', + [MediaType.Game]: 'gameNoteType', + [MediaType.Movie]: 'movieNoteType', + [MediaType.MusicRelease]: 'musicReleaseNoteType', + [MediaType.Season]: 'seasonNoteType', + [MediaType.Series]: 'seriesNoteType', + [MediaType.Song]: 'songNoteType', + [MediaType.Wiki]: 'wikiNoteType', +}; + +/** + * Value written to frontmatter `type` for this media kind. Falls back to the internal + * {@link MediaType} string when the setting is empty. + */ +export function noteTypeValueForMedia(settings: MediaDbPluginSettings, mediaType: MediaType): string { + const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; + const raw = settings[key]; + const s = typeof raw === 'string' ? raw.trim() : ''; + return s !== '' ? s : mediaType; +} + +export function setNoteTypeForMedia(settings: MediaDbPluginSettings, mediaType: MediaType, value: string): void { + const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; + (settings as unknown as Record)[key as string] = value; +} + +/** + * Maps a frontmatter `type` string (legacy enum id or configured custom string) to {@link MediaType}. + */ +export function resolveMetadataTypeToMediaType( + settings: MediaDbPluginSettings, + noteType: unknown, +): MediaType | undefined { + if (noteType === undefined || noteType === null) { + return undefined; + } + let s = String(noteType).trim(); + if (s === '') { + return undefined; + } + if (s === 'manga') { + s = MediaType.ComicManga; + } + for (const mt of MEDIA_TYPES) { + if (mt === s) { + return mt; + } + } + for (const mt of MEDIA_TYPES) { + if (noteTypeValueForMedia(settings, mt) === s) { + return mt; + } + } + return undefined; +} From 774fceb5aae0a001b8e08b0d1e00379513459be9 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:14 +0300 Subject: [PATCH 33/60] Delete main.ts --- main.ts | 1266 ------------------------------------------------------- 1 file changed, 1266 deletions(-) delete mode 100644 main.ts diff --git a/main.ts b/main.ts deleted file mode 100644 index 8705b47c..00000000 --- a/main.ts +++ /dev/null @@ -1,1266 +0,0 @@ -import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder, TFile } from 'obsidian'; -import { requestUrl, normalizePath } from 'obsidian'; -import { MediaType } from 'src/utils/MediaType'; -import { APIManager } from './api/APIManager'; -import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './api/musicBrainzConstants'; -import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; -import { ComicVineAPI } from './api/apis/ComicVineAPI'; -import { GiantBombAPI } from './api/apis/GiantBombAPI'; -import { IGDBAPI } from './api/apis/IGDBAPI'; -import { MALAPI } from './api/apis/MALAPI'; -import { MALAPIManga } from './api/apis/MALAPIManga'; -import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; -import { MusicBrainzAPI } from './api/apis/MusicBrainzAPI'; -import { MusicBrainzArtistAPI } from './api/apis/MusicBrainzArtistAPI'; -import { OMDbAPI } from './api/apis/OMDbAPI'; -import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; -import { RAWGAPI } from './api/apis/RAWGAPI'; -import { SteamAPI } from './api/apis/SteamAPI'; -import { TMDBMovieAPI } from './api/apis/TMDBMovieAPI'; -import { TMDBSeasonAPI } from './api/apis/TMDBSeasonAPI'; -import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; -import { VNDBAPI } from './api/apis/VNDBAPI'; -import { WikipediaAPI } from './api/apis/WikipediaAPI'; -import { GeniusClient } from './api/GeniusClient'; -import { SpotifyClient } from './api/SpotifyClient'; -import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; -import { BulkUpdateConfirmModal } from './modals/BulkUpdateConfirmModal'; -import { CompletionModal } from './modals/CompletionModal'; -import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; -import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; -import type { ArtistModel } from './models/ArtistModel'; -import type { MediaTypeModel } from './models/MediaTypeModel'; -import type { MusicReleaseModel } from './models/MusicReleaseModel'; -import type { SeasonModel } from './models/SeasonModel'; -import { SongModel } from './models/SongModel'; -import { ApiSecretID, getApiSecretValue } from './settings/apiSecretsHelper'; -import { PropertyMapper } from './settings/PropertyMapper'; -import { PropertyMappingModel } from './settings/PropertyMapping'; -import type { MediaDbPluginSettings } from './settings/Settings'; -import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOrder } from './settings/Settings'; -import { AutoTrackerHelper } from './utils/AutoTrackerHelper'; -import { BulkImportHelper } from './utils/BulkImportHelper'; -import { BulkUpdateHelper } from './utils/BulkUpdateHelper'; -import { DateFormatter } from './utils/DateFormatter'; -import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; -import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; -import type { SearchModalOptions } from './utils/ModalHelper'; -import { ModalHelper } from './utils/ModalHelper'; -import type { CreateNoteOptions } from './utils/Utils'; -import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; -import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable } from './utils/Utils'; -import 'src/styles.css'; - -export type Metadata = Record; - -export interface MediaTypeModelObj { - id: string; - type: MediaType; - dataSource: string; -} - -export default class MediaDbPlugin extends Plugin { - settings!: MediaDbPluginSettings; - apiManager!: APIManager; - mediaTypeManager!: MediaTypeManager; - modelPropertyMapper!: PropertyMapper; - modalHelper!: ModalHelper; - bulkImportHelper!: BulkImportHelper; - bulkUpdateHelper!: BulkUpdateHelper; - autoTrackerHelper!: AutoTrackerHelper; - dateFormatter!: DateFormatter; - - frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; - - async onload(): Promise { - this.apiManager = new APIManager(); - // register APIs - this.apiManager.registerAPI(new OMDbAPI(this)); - this.apiManager.registerAPI(new MALAPI(this)); - this.apiManager.registerAPI(new MALAPIManga(this)); - this.apiManager.registerAPI(new WikipediaAPI(this)); - this.apiManager.registerAPI(new MusicBrainzAPI(this)); - this.apiManager.registerAPI(new MusicBrainzArtistAPI(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)); - this.apiManager.registerAPI(new MobyGamesAPI(this)); - this.apiManager.registerAPI(new GiantBombAPI(this)); - this.apiManager.registerAPI(new IGDBAPI(this)); - this.apiManager.registerAPI(new RAWGAPI(this)); - this.apiManager.registerAPI(new VNDBAPI(this)); - - this.mediaTypeManager = new MediaTypeManager(); - this.modelPropertyMapper = new PropertyMapper(this); - this.modalHelper = new ModalHelper(this); - this.bulkImportHelper = new BulkImportHelper(this); - this.bulkUpdateHelper = new BulkUpdateHelper(this); - this.autoTrackerHelper = new AutoTrackerHelper(this); - this.dateFormatter = new DateFormatter(); - - await this.loadSettings(); - // register the settings tab - this.addSettingTab(new MediaDbSettingTab(this.app, this)); - - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - // add icon to the left ribbon and auto-tracker logic - this.refreshAutoTrackerRibbon(); - - this.app.workspace.onLayoutReady(() => { - if (this.settings.autoUpdateAiringMode) { - setTimeout(() => { - this.autoTrackerHelper.startBackgroundScan(true); - }, 5000); - } - }); - - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file) => { - if (file instanceof TFolder) { - // Add our customized context menu options under a "Media DB" group - menu.addItem(item => { - item.setTitle('Media DB...'); - item.setIcon('database'); - // @ts-ignore - if (typeof item.setSubmenu === 'function') { - // @ts-ignore - const sub = item.setSubmenu(); - sub.addItem((subItem: any) => subItem.setTitle('Bulk Import Folder').setIcon('database').onClick(() => this.bulkImportHelper.import(file))); - sub.addItem((subItem: any) => subItem.setTitle('Bulk Update Metadata').setIcon('refresh-cw').onClick(() => this.bulkUpdateHelper.updateFolder(file))); - sub.addItem((subItem: any) => subItem.setTitle('Start Auto-Tracker in Folder').setIcon('sync').onClick(() => { - new BulkUpdateConfirmModal( - this.app, - (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); - }).open(); - })); - sub.addItem((subItem: any) => subItem.setTitle('Download images in folder').setIcon('image').onClick(() => this.downloadImagesInFolder(file))); - } else { - // Fallback if setSubmenu isn't in older Obsidian versions - item.onClick(() => this.bulkUpdateHelper.updateFolder(file)); - } - }); - } - }), - ); - - this.addCommand({ - id: 'media-db-bulk-import-active-file-folder', - name: 'Media DB: Bulk Import Folder (Active Context)', - checkCallback: (checking: boolean) => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile?.parent) return false; - if (!checking) void this.bulkImportHelper.import(activeFile.parent); - return true; - }, - }); - - this.addCommand({ - id: 'media-db-bulk-update-active-file-folder', - name: 'Media DB: Bulk Update Metadata (Active Context)', - checkCallback: (checking: boolean) => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile?.parent) return false; - if (!checking) void this.bulkUpdateHelper.updateFolder(activeFile.parent); - return true; - }, - }); - - this.addCommand({ - id: 'media-db-download-images-active-file-folder', - name: 'Media DB: Download images in folder (Active Context)', - checkCallback: (checking: boolean) => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile?.parent) return false; - if (!checking) void this.downloadImagesInFolder(activeFile.parent); - return true; - }, - }); - - this.addCommand({ - id: 'media-db-download-images-active-note', - name: 'Media DB: Download images in active note', - checkCallback: (checking: boolean) => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') return false; - if (!checking) void this.downloadImagesInFile(activeFile); - return true; - }, - }); - - this.addCommand({ - id: 'media-db-manual-sync-auto-tracker', - name: 'Media DB: Force Auto-Tracker Background Scan', - callback: () => this.autoTrackerHelper.startBackgroundScan(false), - }); - - // register command to open search modal - this.addCommand({ - id: 'open-media-db-search-modal', - name: 'Create Media DB entry', - callback: () => this.createEntryWithSearchModal(), - }); - for (const mediaType of MEDIA_TYPES) { - this.addCommand({ - id: `open-media-db-search-modal-with-${mediaType}`, - name: `Create Media DB entry: ${unCamelCase(mediaType)}`, - callback: () => this.createEntryWithSearchModal({ preselectedTypes: [mediaType] }), - }); - } - this.addCommand({ - id: 'open-media-db-advanced-search-modal', - name: 'Create Media DB entry (advanced search)', - callback: () => this.createEntryWithAdvancedSearchModal(), - }); - // register command to open id search modal - this.addCommand({ - id: 'open-media-db-id-search-modal', - name: 'Create Media DB entry by id', - callback: () => this.createEntryWithIdSearchModal(), - }); - // register command to update the open note - this.addCommand({ - id: 'update-media-db-note', - name: 'Update open note (this will recreate the note)', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(false); - } - return true; - }, - }); - this.addCommand({ - id: 'update-media-db-note-metadata', - name: 'Update metadata', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.updateActiveNote(true); - } - return true; - }, - }); - // register link insert command - this.addCommand({ - id: 'add-media-db-link', - name: 'Insert link', - checkCallback: (checking: boolean) => { - if (!this.app.workspace.getActiveFile()) { - return false; - } - if (!checking) { - void this.createLinkWithSearchModal(); - } - return true; - }, - }); - } - - async createLinkWithSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults) { - return; - } - - const selectResults = await this.modalHelper.openSelectModal({ elements: apiSearchResults, multiSelect: false }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - }); - - if (!selectResults || selectResults.length < 1) { - return; - } - - const link = `[${selectResults[0].title}](${selectResults[0].url})`; - - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - - // Make sure the user is editing a Markdown file. - if (view) { - view.editor.replaceRange(link, view.editor.getCursor()); - } - } - - async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { - let types: string[] = []; - let apiSearchResults = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { - types = searchModalData.types; - const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); - try { - return await this.apiManager.query(searchModalData.query, apis); - } catch (e) { - console.warn('MDB | Query failed:', e); - new Notice(`Search failed: ${e}`); - return []; - } - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - // filter the results - apiSearchResults = apiSearchResults.filter(x => types.contains(x.type)); - - if (apiSearchResults.length === 0) { - new Notice('No results found for the selected types.'); - return; - } - - // Show selection modal - for seasons, skip detail query - const selectResults = - types.length === 1 && types[0] === 'season' - ? await this.modalHelper.openSelectModal( - { - elements: apiSearchResults, - description: 'Select one search result to proceed.', - submitButtonText: 'Ok', - }, - async selectModalData => selectModalData.selected, - ) - : await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => this.queryDetails(selectModalData.selected)); - - if (!selectResults || selectResults.length === 0) { - return; - } - - // Handle season selection for both direct season searches and series-to-season conversion - const seasonHandlingResult = await this.handleSeasonWorkflow(types, selectResults); - if (seasonHandlingResult.handled) { - return; - } - - // Show preview and confirm - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => previewModalData.confirmed); - if (!confirmed) { - return; - } - - // User confirmed, create notes and exit - await this.createMediaDbNotes(selectResults); - } - - /** - * Handles the season workflow for both direct season searches and series-to-season conversion. - * Returns an object indicating what happened and how to proceed. - */ - private async handleSeasonWorkflow(types: string[], selectResults: MediaTypeModel[]): Promise<{ handled: boolean; seasonsCreated?: boolean }> { - // Case 1: User searched specifically for seasons and selected a series from TMDB - if (types.length === 1 && types[0] === 'season' && selectResults.length === 1 && selectResults[0].dataSource === 'TMDBSeasonAPI') { - const created = await this.showSeasonSelectAndCreate(selectResults[0].id, selectResults[0].englishTitle || selectResults[0].title); - return { handled: true, seasonsCreated: created }; - } - - // Case 2: User searched for series but it's actually from TMDBSeasonAPI - // (This happens when searching for seasons returns series results) - if (types.includes('series') && selectResults.some(r => r.dataSource === 'TMDBSeriesAPI')) { - const seriesResults = selectResults.filter(r => r.dataSource === 'TMDBSeriesAPI'); - // If only one series result and user searched for seasons, show season selection - if (seriesResults.length === 1 && types.includes('season')) { - const created = await this.showSeasonSelectAndCreate(seriesResults[0].id, seriesResults[0].title); - return { handled: true, seasonsCreated: created }; - } - } - - return { handled: false }; - } - - /** - * Shows the season selection modal for a given series and creates notes for selected seasons. - * Returns true if seasons were successfully created, false if cancelled. - */ - private async showSeasonSelectAndCreate(seriesId: string, seriesTitle: string): Promise { - const tmdbSeasonAPI = this.apiManager.getApiByName('TMDBSeasonAPI') as TMDBSeasonAPI; - if (!tmdbSeasonAPI) { - new Notice('TMDBSeasonAPI not available.'); - return false; - } - - try { - // Fetch all seasons for the selected series - const allSeasons = await tmdbSeasonAPI.getSeasonsForSeries(seriesId); - if (!allSeasons || allSeasons.length === 0) { - new Notice('No seasons found for this series.'); - return false; - } - - // Show season selection modal - const selectedSeasons = await this.showSeasonSelectModal(allSeasons, seriesTitle); - if (!selectedSeasons || selectedSeasons.length === 0) { - return false; - } - - // Create notes for all selected seasons in parallel - await this.createNotesForSelectedSeasons(selectedSeasons, allSeasons, tmdbSeasonAPI); - new Notice(`Successfully created ${selectedSeasons.length} season ${selectedSeasons.length === 1 ? 'entry' : 'entries'}.`); - return true; - } catch (e) { - console.warn('MDB | Error in season selection workflow:', e); - new Notice(`Error loading seasons: ${e}`); - return false; - } - } - - /** - * Shows the season selection modal and returns the selected seasons. - */ - private async showSeasonSelectModal(allSeasons: SeasonModel[], seriesTitle: string): Promise { - const modal = new MediaDbSeasonSelectModal( - this, - allSeasons.map(s => ({ - season_number: s.seasonNumber, - name: s.seasonTitle || s.title, - episode_count: s.episodes || 0, - air_date: s.year > 0 ? String(s.year) : 'unknown', - poster_path: s.image, - })), - true, - seriesTitle, - ); - - return new Promise(resolve => { - modal.setSubmitCb(resolve); - modal.open(); - }); - } - - /** - * Creates notes for all selected seasons by fetching full metadata and creating entries. - */ - private async createNotesForSelectedSeasons(selectedSeasons: SeasonSelectModalElement[], allSeasons: SeasonModel[], tmdbSeasonAPI: TMDBSeasonAPI): Promise { - await Promise.all( - selectedSeasons.map(async selectedSeason => { - const seasonModel = allSeasons.find(s => s.seasonNumber === selectedSeason.season_number); - if (seasonModel) { - try { - // Fetch full metadata using getById - const fullMetadata = await tmdbSeasonAPI.getById(seasonModel.id); - await this.createMediaDbNotes([fullMetadata]); - } catch (e) { - console.warn(`MDB | Failed to create season ${selectedSeason.season_number}:`, e); - new Notice(`Failed to create season ${selectedSeason.season_number}: ${e}`); - } - } - }), - ); - } - - async createEntryWithAdvancedSearchModal(): Promise { - const apiSearchResults = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { - return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); - }); - - if (!apiSearchResults || apiSearchResults.length === 0) { - new Notice('No results found.'); - return; - } - - let selectResults: MediaTypeModel[]; - const proceed: boolean = false; - - while (!proceed) { - selectResults = - (await this.modalHelper.openSelectModal({ elements: apiSearchResults }, async selectModalData => { - return await this.queryDetails(selectModalData.selected); - })) ?? []; - if (!selectResults || selectResults.length < 1) { - return; - } - - const confirmed = await this.modalHelper.openPreviewModal({ elements: selectResults }, async previewModalData => { - return previewModalData.confirmed; - }); - if (!confirmed) { - return; - } - break; - } - - await this.createMediaDbNotes(selectResults!); - } - - async createEntryWithIdSearchModal(): Promise { - let idSearchResult: MediaTypeModel | undefined = undefined; - let proceed: boolean = false; - - while (!proceed) { - idSearchResult = await this.modalHelper.openIdSearchModal({}, async idSearchModalData => { - return await this.apiManager.queryDetailedInfoById(idSearchModalData.query, idSearchModalData.api); - }); - if (!idSearchResult) { - return; - } - - proceed = await this.modalHelper.openPreviewModal({ elements: [idSearchResult] }, async previewModalData => { - return previewModalData.confirmed; - }); - } - - if (!idSearchResult) { - return; - } - await this.createMediaDbNoteFromModel(idSearchResult, { attachTemplate: true, openNote: true }); - } - - async createMediaDbNotes(models: MediaTypeModel[], attachFile?: TFile): Promise { - const hasArtist = models.some(m => m.getMediaType() === MediaType.Artist); - - if (hasArtist) { - for (const model of models) { - await this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }); - } - return; - } - - const results = await Promise.allSettled(models.map(model => this.createMediaDbNoteFromModel(model, { attachTemplate: true, attachFile: attachFile }))); - - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some notes failed to create:', failures); - new Notice(`${models.length - failures.length} of ${models.length} notes created successfully.`); - } - } - - async queryDetails(models: MediaTypeModel[]): Promise { - // Query details in parallel for better performance - const results = await Promise.allSettled(models.map(model => this.apiManager.queryDetailedInfo(model))); - - // Filter out failures and return successful results - const detailModels: MediaTypeModel[] = results - .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled' && r.value !== undefined) - .map(r => r.value!); - - // Log failures for debugging - const failures = results.filter(r => r.status === 'rejected'); - if (failures.length > 0) { - console.warn('MDB | Some detail queries failed:', failures); - } - - return detailModels; - } - - async createMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - if (mediaTypeModel.getMediaType() === MediaType.Artist) { - await this.importArtistDiscography(mediaTypeModel as ArtistModel, options); - return; - } - - await this.createStandardMediaDbNoteFromModel(mediaTypeModel, options); - } - - /** @returns whether the note file was created (false if the user cancelled overwrite or an error occurred before the file was written). */ - private async createStandardMediaDbNoteFromModel(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - try { - console.debug('MDB | creating new note'); - - options.openNote ??= this.settings.openNoteInNewTab; - - if (this.settings.imageDownload) { - await this.downloadImageForMediaModel(mediaTypeModel); - } - - const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); - - options.folder ??= await this.mediaTypeManager.getFolder(mediaTypeModel, this.app); - - const targetFile = await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); - - if (this.settings.enableTemplaterIntegration) { - try { - await useTemplaterPluginInFile(this.app, targetFile); - } catch (e) { - console.warn(e); - new Notice(`${e}`); - } - } - return true; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - return false; - } - } - - private safeFileTreeSegment(title: string): string { - return replaceIllegalFileNameCharactersInString(title).replaceAll(/ +/g, ' ').trim(); - } - - private async ensureVaultFolder(folderPath: string): Promise { - const normalized = normalizePath(folderPath); - if (!(await this.app.vault.adapter.exists(normalized))) { - await this.app.vault.createFolder(normalized); - } - const folder = this.app.vault.getAbstractFileByPath(normalized); - if (!(folder instanceof TFolder)) { - throw new Error(`MDB | Expected folder at ${normalized}`); - } - return folder; - } - - private async importArtistDiscography(artist: ArtistModel, options: CreateNoteOptions): Promise { - try { - const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; - const genius = new GeniusClient(geniusToken); - if (!genius.isConfigured()) { - new Notice('Artist import: add a Genius API access token in settings to fetch lyrics.'); - } - - const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; - const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; - const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); - - const artistApi = this.apiManager.getApiByName('MusicBrainz Artist API') as MusicBrainzArtistAPI | undefined; - const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; - if (!artistApi || !musicBrainzApi) { - new Notice('MusicBrainz APIs not available.'); - return; - } - - const useTree = this.settings.artistUseFileTreeForSongs; - const childOptions: CreateNoteOptions = { - attachTemplate: true, - openNote: false, - attachFile: undefined, - folder: undefined, - }; - - const artistBaseFolder = await this.mediaTypeManager.getFolder(artist, this.app); - const artistNoteFolder = artistBaseFolder; - let albumNotesFolder = artistBaseFolder; - - if (useTree) { - const artistSeg = this.safeFileTreeSegment(artist.title); - const treeRootPath = normalizePath(`${artistBaseFolder.path}/${artistSeg}`); - albumNotesFolder = await this.ensureVaultFolder(treeRootPath); - } - - const artistNoteCreated = await this.createStandardMediaDbNoteFromModel(artist, { ...options, folder: artistNoteFolder }); - if (!artistNoteCreated) { - return; - } - - let releaseGroupIds: string[]; - try { - releaseGroupIds = await artistApi.listStudioAlbumReleaseGroupIds(artist.id); - } catch (e) { - new Notice(`Could not load albums: ${e}`); - return; - } - - new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${artist.title}…`); - - for (const rgId of releaseGroupIds) { - await new Promise(r => setTimeout(r, 1100)); - let release: MusicReleaseModel; - try { - const model = await musicBrainzApi.getById(rgId); - release = model as MusicReleaseModel; - } catch (e) { - console.warn(`MDB | Skipping release group ${rgId}:`, e); - continue; - } - - let songNotesFolder: TFolder | undefined; - if (useTree) { - const albumSeg = this.safeFileTreeSegment(release.title); - songNotesFolder = await this.ensureVaultFolder(normalizePath(`${albumNotesFolder.path}/${albumSeg}`)); - } - - const releaseOpts: CreateNoteOptions = useTree ? { ...childOptions, folder: albumNotesFolder } : { ...childOptions }; - - const albumNoteCreated = await this.createStandardMediaDbNoteFromModel(release, releaseOpts); - if (!albumNoteCreated) { - continue; - } - - for (const track of release.tracks) { - let lyrics = ''; - let geniusUrl = ''; - if (genius.isConfigured()) { - await new Promise(r => setTimeout(r, 500)); - const hit = await genius.searchFirstSongHit(`${artist.title} ${track.title}`); - if (hit) { - geniusUrl = hit.url; - await new Promise(r => setTimeout(r, 600)); - lyrics = await genius.fetchLyricsFromSongPage(hit.url); - } - } - - let spotifyUrl = ''; - if (track.recordingId) { - await new Promise(r => setTimeout(r, 1100)); - try { - spotifyUrl = await musicBrainzApi.fetchSpotifyUrlForRecording(track.recordingId); - } catch (e) { - console.warn(`MDB | Spotify URL for recording ${track.recordingId}:`, e); - } - } - if (!spotifyUrl && spotify.isConfigured()) { - const primaryArtist = release.artists[0] ?? artist.title; - console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); - try { - spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); - } catch (e) { - console.warn(`MDB | Spotify search for "${track.title}":`, e); - } - } - - const song = new SongModel({ - type: 'song', - title: track.title, - englishTitle: track.title, - year: release.year, - releaseDate: release.releaseDate, - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: geniusUrl || release.url, - id: `${release.id}-t${track.number}`, - image: release.image, - subType: 'song', - genres: release.genres ?? [], - artists: release.artists.length > 0 ? release.artists : [artist.title], - albumTitle: release.title, - albumReleaseGroupId: release.id, - trackNumber: track.number, - duration: track.duration, - featuredArtists: track.featuredArtists, - geniusUrl, - spotifyUrl, - lyrics, - userData: { personalRating: 0 }, - }); - - const songOpts: CreateNoteOptions = useTree && songNotesFolder ? { ...childOptions, folder: songNotesFolder } : { ...childOptions }; - - await this.createStandardMediaDbNoteFromModel(song, songOpts); - } - } - - new Notice(`Finished artist import for ${artist.title}.`); - } catch (e) { - console.warn(e); - new Notice(`${e}`); - } - } - - /** - * Tries to download the image for a media model. - * - * @param mediaTypeModel - * @returns true if the image was downloaded, false otherwise - */ - private async downloadImageForMediaModel(mediaTypeModel: MediaTypeModel): Promise { - if (mediaTypeModel.image && typeof mediaTypeModel.image === 'string' && mediaTypeModel.image.startsWith('http')) { - try { - const imageUrl = mediaTypeModel.image; - const imageExt = imageUrl.split('.').pop()?.split(/#|\?/)[0] ?? 'jpg'; - const imageFileName = `${replaceIllegalFileNameCharactersInString(`${mediaTypeModel.type}_${mediaTypeModel.title} (${mediaTypeModel.year})`)}.${imageExt}`; - const imagePath = normalizePath(`${this.settings.imageFolder}/${imageFileName}`); - - if (!this.app.vault.getAbstractFileByPath(this.settings.imageFolder)) { - await this.app.vault.createFolder(this.settings.imageFolder); - } - - if (!this.app.vault.getAbstractFileByPath(imagePath)) { - const response = await requestUrl({ url: imageUrl, method: 'GET' }); - await this.app.vault.createBinary(imagePath, response.arrayBuffer); - } - - // Update model to use local image path - mediaTypeModel.image = `[[${imagePath}]]`; - return true; - } catch (e) { - console.warn('MDB | Failed to download image:', e); - } - } - - return false; - } - - async downloadImagesInFolder(folder: TFolder): Promise { - new Notice(`MDB | Scanning for images to download in ${folder.name}...`); - const files = folder.children.filter((c): c is TFile => c instanceof TFile && c.extension === 'md'); - const startTime = Date.now(); - let downloaded = 0; - let failed = 0; - const erroredFiles: { filePath: string, error: string }[] = []; - - for (const file of files) { - const result = await this.downloadImagesInFile(file, true); - if (result.success) { - downloaded++; - } else if (!result.skipped) { - failed++; - if (result.error) erroredFiles.push({ filePath: file.path, error: result.error }); - } - // wait slightly as anti-rate limit - if (!result.skipped) { - await new Promise(r => setTimeout(r, 600)); - } - } - - if (failed > 0 && erroredFiles.length > 0) { - const title = `MDB - image download error report ${dateTimeToString(new Date())}`; - const filePath = `${title}.md`; - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - const fileContent = markdownTable(table); - await this.app.vault.create(filePath, fileContent); - } - - new CompletionModal(this.app, { - title: 'Image Download Complete', - icon: '🖼️', - total: downloaded + failed, - success: downloaded, - errors: failed, - elapsedMs: Date.now() - startTime, - notes: failed > 0 ? ['Some images could not be downloaded. A detailed report file has been created in your vault folder.'] : [], - }).open(); - } - - /** - * Downloads images for a single file. - * @returns object detailing success, possible errors, or whether it was skipped - */ - async downloadImagesInFile(file: TFile, silent: boolean = false): Promise<{ success: boolean; skipped?: boolean; error?: string }> { - const metadata = this.getMetadataFromFileCache(file); - if (typeof metadata.image === 'string' && metadata.image.startsWith('http')) { - try { - const imageUrl = metadata.image; - const extMatch = imageUrl.split('?')[0].match(/\.([a-zA-Z0-9]+)$/); - const ext = extMatch ? extMatch[1] : 'jpg'; - const imgName = replaceIllegalFileNameCharactersInString(file.basename) + '.' + ext; - const imgFolder = await this.ensureVaultFolder(this.settings.imageFolder); - const imagePath = `${imgFolder.path}/${imgName}`; - - if (!this.app.vault.getAbstractFileByPath(imagePath)) { - const response = await requestUrl({ url: imageUrl, method: 'GET' }); - await this.app.vault.createBinary(imagePath, response.arrayBuffer); - } - - await this.app.fileManager.processFrontMatter(file, (frontmatter: any) => { - frontmatter.image = `[[${imagePath}]]`; - }); - if (!silent) new Notice(`MDB | Image downloaded for ${file.basename}`); - return { success: true }; - } catch (e) { - console.error("MDB | Image download failed for", file.path, e); - if (!silent) new Notice(`MDB | Image download failed for ${file.basename}`); - return { success: false, error: `${e}` }; - } - } - if (!silent) new Notice(`MDB | No external image found in ${file.basename}`); - return { success: false, skipped: true }; - } - - private metadataRecordForNewNote(mediaTypeModel: MediaTypeModel): Record { - let meta: Record; - if (this.settings.useDefaultFrontMatter) { - meta = mediaTypeModel.toMetaDataObject(); - } else { - meta = { - id: mediaTypeModel.id, - type: mediaTypeModel.type, - dataSource: mediaTypeModel.dataSource, - }; - } - return meta; - } - - generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { - mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); - const fileMetadata = this.modelPropertyMapper.convertObject(this.metadataRecordForNewNote(mediaTypeModel)); - return stringifyYaml(fileMetadata); - } - - /** - * Generates the content of a note from a media model and some options. - * - * @param mediaTypeModel - * @param options - */ - async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); - - let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - let fileMetadata: Record = this.modelPropertyMapper.convertObject( - this.metadataRecordForNewNote(mediaTypeModel), - ); - - let fileContent = ''; - template = options.attachTemplate ? template : ''; - - ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); - ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); - - // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- - const entityWikiProps = this.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); - if (entityWikiProps.length > 0) { - const folderPrefix = this.settings.wikiFolder ? `${this.settings.wikiFolder}/` : ''; - const isEnabled = this.settings.enableWikiLinkParsing; - const formatWiki = (v: unknown) => { - if (typeof v !== 'string') return v; - let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); - if (clean.includes('|')) clean = clean.split('|')[1]; - return isEnabled ? `[[${folderPrefix}${clean}|${clean}]]` : clean.trim(); - }; - - for (const [key, value] of Object.entries(fileMetadata)) { - if (key === 'aliases') continue; - if (entityWikiProps.includes(key.toLowerCase())) { - if (typeof value === 'string') { - fileMetadata[key] = formatWiki(value); - } else if (Array.isArray(value)) { - fileMetadata[key] = value.map(formatWiki); - } - } - } - } - - // --- Auto-Tag Logic --- - const tagProps = this.settings.autoTagProperties.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); - if (this.settings.enableAutoTagging && tagProps.length > 0) { - const existingTags: string[] = Array.isArray(fileMetadata['tags']) ? (fileMetadata['tags'] as string[]) : []; - const newTags = new Set(existingTags.filter(t => typeof t === 'string' && t.trim() !== '')); - - for (const [key, value] of Object.entries(fileMetadata)) { - if (tagProps.includes(key.toLowerCase()) && value) { - const valuesToTag = Array.isArray(value) ? value : [value]; - for (let v of valuesToTag) { - if (typeof v === 'string') { - v = String(v).replace(/^\[\[(.*?)\]\]$/, '$1'); - if (v.includes('|')) { - v = v.split('|')[1]; - } - const sanitized = v - .trim() - .replace(/\s+/g, '-') - .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') - .toLowerCase(); - - if (sanitized) newTags.add(sanitized); - } - } - } - } - - if (newTags.size > 0) { - fileMetadata['tags'] = Array.from(newTags); - } - } - - if (mediaTypeModel.getMediaType() === MediaType.Song) { - const song = mediaTypeModel as SongModel; - if(song.lyrics.length > 0) { - fileContent += `# Lyrics\n\`\`\`\n${song.lyrics}\n\`\`\`\n`; - } - } - - if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { - // Include the media variable in all templater commands by using a top level JavaScript execution command. - const mediaJson = JSON.stringify(mediaTypeModel, (key, value: unknown) => (key === 'lyrics' ? undefined : value)); - fileContent = `---\n<%* const media = ${mediaJson} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } else { - fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; - } - - return fileContent; - } - - extractManualTags(metadata: Record, autoTagKeys: string): string[] { - const allTagsRaw = metadata['tags']; - const allTags = Array.isArray(allTagsRaw) ? allTagsRaw : typeof allTagsRaw === 'string' ? [allTagsRaw] : []; - if (allTags.length === 0) return []; - - const tagProps = autoTagKeys.split(',').map(s => s.trim().toLowerCase()).filter(s => s); - const autoTagValues = new Set(); - - for (const [key, value] of Object.entries(metadata)) { - if (tagProps.includes(key.toLowerCase()) && value) { - const valuesToTag = Array.isArray(value) ? value : [value]; - for (const v of valuesToTag) { - if (typeof v === 'string') { - let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); - if (clean.includes('|')) clean = clean.split('|')[1]; - const sanitized = clean.trim().replace(/\s+/g, '-').replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '').toLowerCase(); - if (sanitized) autoTagValues.add(sanitized); - } - } - } - } - - return allTags.map(t => String(t).trim()).filter(t => t && !autoTagValues.has(t.toLowerCase()) && !t.toLowerCase().startsWith('mediadb/')); - } - - async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!fileToAttach) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); - - // Rescue arrays that Object.assign would normally crush - const rescueArray = (key: string) => { - const arr = attachFileMetadata[key]; - if (Array.isArray(arr)) return [...arr as string[]]; - if (typeof arr === 'string' && arr.trim()) return [arr]; - return []; - }; - const oldManualTags = this.extractManualTags(attachFileMetadata, this.settings.autoTagProperties); - const oldAliases = rescueArray('aliases'); - - // TODO: better object merging - fileMetadata = Object.assign(attachFileMetadata, fileMetadata); - - // Merge tags cleanly (Preserving only manual user tags, discarding old ghost auto-tags!) - const newObjTags = fileMetadata['tags']; - const finalTags = new Set([...oldManualTags, ...(Array.isArray(newObjTags) ? newObjTags : typeof newObjTags === 'string' ? [newObjTags] : [])].map(t => String(t).trim())); - if (finalTags.size > 0) fileMetadata['tags'] = Array.from(finalTags); - - // Merge aliases cleanly - const newObjAliases = fileMetadata['aliases']; - const finalAliases = new Set([...oldAliases, ...(Array.isArray(newObjAliases) ? newObjAliases : typeof newObjAliases === 'string' ? [newObjAliases] : [])].map(a => String(a).trim())); - if (finalAliases.size > 0) fileMetadata['aliases'] = Array.from(finalAliases); - - let attachFileContent: string = await this.app.vault.read(fileToAttach); - const regExp = new RegExp(this.frontMatterRexExpPattern); - attachFileContent = attachFileContent.replace(regExp, ''); - attachFileContent = attachFileContent.startsWith('\n') ? attachFileContent.substring(1) : attachFileContent; - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string | undefined): Promise<{ fileMetadata: Metadata; fileContent: string }> { - if (!template) { - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - const templateMetadata = this.getMetaDataFromFileContent(template); - // TODO: better object merging - fileMetadata = Object.assign(templateMetadata, fileMetadata); - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const attachFileContent = template.replace(regExp, ''); - fileContent += attachFileContent; - - return { fileMetadata: fileMetadata, fileContent: fileContent }; - } - - getMetaDataFromFileContent(fileContent: string): Metadata { - let metadata: Metadata; - - const regExp = new RegExp(this.frontMatterRexExpPattern); - const frontMatterRegExpResult = regExp.exec(fileContent); - if (!frontMatterRegExpResult) { - return {}; - } - let frontMatter = frontMatterRegExpResult[0]; - if (!frontMatter) { - return {}; - } - frontMatter = frontMatter.substring(4); - frontMatter = frontMatter.substring(0, frontMatter.length - 3); - - metadata = parseYaml(frontMatter) as Metadata; - - if (!metadata) { - metadata = {}; - } - - console.debug(`MDB | metadata read from file content`, metadata); - - return metadata; - } - - getMetadataFromFileCache(file: TFile): Metadata { - const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; - return structuredClone(metadata ?? {}); - } - - /** - * Creates a note in the vault. - * - * @param fileName - * @param fileContent - * @param options - */ - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { - // find and possibly create the folder set in settings or passed in folder - const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); - - if (!folder || !(folder instanceof TFolder)) { - throw new Error('MDB | invalid folder'); - } - - fileName = replaceIllegalFileNameCharactersInString(fileName); - const filePath = `${folder.path}/${fileName}.md`; - - // look if file already exists and ask if it should be overwritten - const file = this.app.vault.getAbstractFileByPath(filePath); - if (file) { - let shouldOverwrite = options.overwrite; - if (!shouldOverwrite) { - shouldOverwrite = await new Promise(resolve => { - new ConfirmOverwriteModal(this.app, fileName, resolve).open(); - }); - } - - if (!shouldOverwrite) { - throw new Error('MDB | file creation cancelled by user'); - } - - await this.app.vault.delete(file); - } - - // create the file - const targetFile = await this.app.vault.create(filePath, fileContent); - console.debug(`MDB | created new file at ${filePath}`); - - // open newly created file - if (options.openNote) { - const activeLeaf = this.app.workspace.getUnpinnedLeaf(); - if (!activeLeaf) { - console.warn('MDB | no active leaf, not opening newly created note'); - return targetFile; - } - await activeLeaf.openFile(targetFile, { state: { mode: 'source' } }); - } - - return targetFile; - } - - // --- AutoTracker Ribbon Logic --- - public _ribbonEl: HTMLElement | null = null; - refreshAutoTrackerRibbon() { - if (!this._ribbonEl) { - this._ribbonEl = this.addRibbonIcon('sync', 'Media DB: Auto-Tracker Sync', () => { - if (this.autoTrackerHelper.isScanning) { - new Notice("Auto-Tracker is currently syncing in the background."); - } else { - new BulkUpdateConfirmModal( - this.app, - (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate); - }).open(); - } - }); - this._ribbonEl.addClass('obsidian-media-db-plugin-ribbon-class'); - } - - if (this.autoTrackerHelper.isScanning) { - this._ribbonEl.addClass('media-db-spin-animation'); - } else { - this._ribbonEl.removeClass('media-db-spin-animation'); - } - } - - /** - * Update the active note by querying the API again. - * Tries to read the type and id of the active note (and dataSource when required). If successful it will query the api, delete the old note and create a new one. - */ - async updateActiveNote(onlyMetadata: boolean = false): Promise { - const activeFile = this.app.workspace.getActiveFile() ?? undefined; - if (!activeFile) { - throw new Error('MDB | there is no active note'); - } - return this.updateNote(activeFile, onlyMetadata, true, false); - } - - async updateNote(activeFile: TFile, onlyMetadata: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { - - let metadata = this.getMetadataFromFileCache(activeFile); - metadata = this.modelPropertyMapper.convertObjectBack(metadata); - - console.debug(`MDB | read metadata`, metadata); - - if (!metadata?.type || !metadata?.id) { - throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); - } - - const mediaType = resolveMetadataTypeToMediaType(this.settings, metadata.type); - if (mediaType === undefined) { - throw new Error('MDB | active note type is not recognized; check Settings → Note type for each media kind'); - } - let dataSource = typeof metadata.dataSource === 'string' ? metadata.dataSource.trim() : ''; - if ( - !dataSource && - musicBrainzRegisteredApiName(mediaType) - ) { - dataSource = MUSICBRAINZ_NOTE_DATA_SOURCE; - } - if (!dataSource) { - throw new Error('MDB | active note is missing dataSource (required for this media type)'); - } - - const validOldMetadata: MediaTypeModelObj = { ...metadata, dataSource } as unknown as MediaTypeModelObj; - console.debug(`MDB | validOldMetadata`, validOldMetadata); - - const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, mediaType); - console.debug(`MDB | oldMediaTypeModel created`, oldMediaTypeModel); - - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource, mediaType); - if (!newMediaTypeModel) { - return; - } - - newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); - console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); - - if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); - } else { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); - } - } - - async loadSettings(): Promise { - const diskSettings: MediaDbPluginSettings = (await this.loadData()) as MediaDbPluginSettings; - const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); - const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); - - // Migrate property mappings using the dedicated migration method - const migratedModels = PropertyMappingModel.migrateModels( - loadedSettings.propertyMappingModels || [], - defaultSettings.propertyMappingModels.map(m => PropertyMappingModel.fromJSON(m)), - ); - - // Store as plain data for serialization (canonical order matches settings UI) - loadedSettings.propertyMappingModels = propertyMappingModelsInDisplayOrder(migratedModels.map(m => m.toJSON())); - - // --- MIGRATION: Band to Artist --- - const anyLoaded = diskSettings as any; - if (anyLoaded) { - if (anyLoaded.bandTemplate && !loadedSettings.artistTemplate) loadedSettings.artistTemplate = anyLoaded.bandTemplate; - if (anyLoaded.bandFolder && !loadedSettings.artistFolder) loadedSettings.artistFolder = anyLoaded.bandFolder; - if (anyLoaded.bandFileNameTemplate && !loadedSettings.artistFileNameTemplate) loadedSettings.artistFileNameTemplate = anyLoaded.bandFileNameTemplate; - if (anyLoaded.bandNoteType && !loadedSettings.artistNoteType) loadedSettings.artistNoteType = anyLoaded.bandNoteType; - if (anyLoaded.bandUseFileTreeForSongs !== undefined && loadedSettings.artistUseFileTreeForSongs === false) loadedSettings.artistUseFileTreeForSongs = anyLoaded.bandUseFileTreeForSongs; - if (anyLoaded.MusicBrainzBandAPI_disabledMediaTypes && !loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes) loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes = anyLoaded.MusicBrainzBandAPI_disabledMediaTypes; - } - - this.settings = loadedSettings; - } - - async saveSettings(): Promise { - this.mediaTypeManager.updateTemplates(this.settings); - this.mediaTypeManager.updateFolders(this.settings); - this.dateFormatter.setFormat(this.settings.customDateFormat); - - await this.saveData(this.settings); - } -} From 45b27a2371189110ca1f8d957016759c947cd5d3 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:20 +0300 Subject: [PATCH 34/60] Delete styles.css --- styles.css | 438 ----------------------------------------------------- 1 file changed, 438 deletions(-) delete mode 100644 styles.css diff --git a/styles.css b/styles.css deleted file mode 100644 index 5c0c2e6a..00000000 --- a/styles.css +++ /dev/null @@ -1,438 +0,0 @@ -.media-db-plugin-list-wrapper { - display: flex; - align-content: center; - margin-bottom: 5px; - margin-top: 5px; -} - -.media-db-plugin-list-toggle { -} - -.media-db-plugin-list-text-wrapper { - flex: 1; -} - -.media-db-plugin-list-text { - display: block; -} - -small.media-db-plugin-list-text { - color: var(--text-muted); -} - -.media-db-plugin-select-modal { - display: contents; -} - -.media-db-plugin-select-wrapper { - display: flex; - flex-direction: column; - margin: 5px; - overflow-y: auto; -} - -.media-db-plugin-select-element { - cursor: pointer; - border-left: 5px solid transparent; - padding: 5px; - margin: 5px 0 5px 0; - border-radius: 5px; - white-space: pre-wrap; - font-size: 16px; -} - -.media-db-plugin-select-element-selected { - border-left: 5px solid var(--interactive-accent) !important; - background: var(--background-secondary-alt); -} - -.media-db-plugin-select-element-hover { - background: var(--background-secondary-alt); -} - -.media-db-plugin-preview-modal { - display: contents; -} - -.media-db-plugin-preview-wrapper { - display: flex; - flex-direction: column; - overflow-y: auto; -} - -.media-db-plugin-spacer { - margin-bottom: 10px; -} - -.media-db-plugin-button:focus { - /*outline: 1px solid white;*/ -} - -.media-db-plugin-preview { - border-radius: var(--modal-radius); - border: var(--modal-border-width) solid var(--modal-border-color); - padding: var(--size-4-4); -} - -/* Icon Component Styles */ -.icon-wrapper { - display: inline-block; - position: relative; - width: 20px; -} - -.icon { - position: absolute; - height: 20px; - width: 20px; - top: calc(50% - 10px); -} - -/* Property Mapping Component Styles */ -.media-db-plugin-property-mappings-model-container { - margin-bottom: var(--size-4-8); -} - -.media-db-plugin-property-mappings-model-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--size-4-4); - gap: var(--size-4-3); -} - -.media-db-plugin-property-mappings-model-header--actions-only { - justify-content: flex-end; -} - -.media-db-plugin-property-mappings-model-header .setting-item-name { - font-weight: var(--font-semibold); - font-size: var(--font-ui-medium); - color: var(--text-normal); - margin: 0; -} - -.media-db-plugin-property-mappings-model-actions { - display: flex; - align-items: center; - gap: var(--size-4-3); -} - -.media-db-plugin-property-mapping-unsaved-changes { - color: var(--text-warning); - font-size: var(--font-ui-small); - white-space: nowrap; -} - -.media-db-plugin-property-mappings-save-button { - white-space: nowrap; - cursor: pointer; -} - -.media-db-plugin-property-mappings-save-button.mod-muted { - opacity: 0.5; - cursor: not-allowed; -} - -.media-db-plugin-property-mapping-validation { - color: var(--text-error); - background: rgba(var(--color-red-rgb), 0.1); - padding: var(--size-4-3) var(--size-4-4); - margin-bottom: var(--size-4-4); - border-left: 3px solid var(--text-error); - font-size: var(--font-ui-small); - line-height: 1.5; - border-radius: var(--radius-s); -} - -.media-db-plugin-property-mappings-table-container { - overflow-x: auto; -} - -.media-db-plugin-property-mappings-table { - width: 100%; - border-collapse: collapse; - border-spacing: 0; - font-size: var(--font-ui-small); -} - -.media-db-plugin-property-mappings-table thead { - border-bottom: 1px solid var(--background-modifier-border); -} - -.media-db-plugin-property-mappings-table th { - padding: var(--size-4-2) var(--size-4-3); - padding-left: 0; - text-align: left; - font-weight: var(--font-semibold); - color: var(--text-muted); - font-size: var(--font-ui-smaller); - text-transform: uppercase; - letter-spacing: 0.02em; - border-bottom: none; -} - -.media-db-plugin-property-mappings-table tbody tr { - transition: background-color 0.1s ease; -} - -.media-db-plugin-property-mappings-table td { - padding: var(--size-4-3) var(--size-4-3) var(--size-4-3) 0; - border-bottom: 1px solid var(--background-modifier-border-hover); - vertical-align: middle; -} - -.media-db-plugin-property-mappings-table tbody tr:last-child td { - border-bottom: none; -} - -.col-property { - width: 25%; - white-space: nowrap; -} - -.col-mapping { - width: 20%; -} - -.col-new-name { - width: 40%; -} - -.col-wikilink { - width: 15%; - text-align: center; -} - -.col-locked { - text-align: center; - font-style: italic; -} - -.media-db-plugin-property-mappings-table code { - padding: var(--size-4-1) var(--size-4-2); - margin: 0; - background: var(--code-background); - color: var(--code-normal); - border-radius: var(--radius-s); - font-size: var(--font-ui-smaller); - font-family: var(--font-monospace); -} - -.media-db-plugin-property-binding-text { - color: var(--text-muted); - font-size: var(--font-ui-small); - font-style: italic; -} - -.media-db-plugin-property-mappings-table select.dropdown { - width: 100%; - max-width: 100%; -} - -.media-db-plugin-property-mapping-to { - display: flex; - align-items: center; - gap: var(--size-4-2); - min-width: 0; -} - -.media-db-plugin-property-mapping-input { - flex: 1; - width: 100%; - font-family: var(--font-monospace); -} - -.media-db-plugin-property-mapping-to-disabled { - color: var(--text-faint); - font-size: var(--font-ui-medium); -} - -.media-db-plugin-property-mapping-wikilink-label { - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - padding: var(--size-4-1); -} - -.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'] { - cursor: pointer; - width: var(--checkbox-size); - height: var(--checkbox-size); -} - -/* - * Settings tabs: same layout pattern as Obsidian Linter (horizontal icon tabs). - * Adapted from https://github.com/platers/obsidian-linter/blob/master/src/styles.css (MIT). - */ -.media-db-navigation-item { - cursor: pointer; - border-radius: 8px 8px 2px 2px; - border: 1px solid var(--background-modifier-border); - font-weight: bold; - font-size: 16px; - display: flex; - flex-direction: row; - white-space: nowrap; - padding: 4px 6px; - align-items: center; - gap: 4px; - overflow: hidden; - background-color: var(--background-primary-secondary-alt); - transition: - color 0.25s ease-in-out, - padding 0.25s ease-in-out, - background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), - max-width 0.35s cubic-bezier(0.57, 0.04, 0.58, 1); - height: 32px; -} - -@media screen and (max-width: 1325px) { - .media-db-navigation-item.media-db-desktop { - max-width: 32px; - } -} - -@media screen and (max-width: 800px) { - .media-db-navigation-item.media-db-mobile { - max-width: 32px; - } -} - -.media-db-navigation-item-icon { - padding-top: 5px; -} - -.media-db-navigation-item:hover { - border-color: var(--interactive-accent-hover); - border-bottom: 0; -} - -.media-db-navigation-item-selected { - background-color: var(--interactive-accent) !important; - color: var(--text-on-accent); - padding: 4px 9px !important; - max-width: 100% !important; - border: 1px solid var(--background-modifier-border); - border-radius: 8px 8px 2px 2px; - border-bottom: 0; - transition: - color 0.25s ease-in-out, - padding 0.25s ease-in-out, - background-color 0.35s cubic-bezier(0.45, 0.25, 0.83, 0.67), - max-width 0.45s cubic-bezier(0.57, 0.04, 0.58, 1) 0.2s; -} - -.media-db-setting-header { - margin-bottom: 24px; - overflow-y: hidden; - overflow-x: auto; -} - -.media-db-setting-header .media-db-setting-tab-group { - display: flex; - align-items: flex-end; - flex-wrap: wrap; - width: 100%; -} - -.media-db-setting-tab-group { - margin-top: 6px; - padding-left: 2px; - padding-right: 2px; - border-bottom: 2px solid var(--background-modifier-border); -} - -.media-db-tab-settings--hidden { - display: none !important; -} - -.media-db-navigation-item:not(.media-db-navigation-item-selected) > span:nth-child(2), -.media-db-visually-hidden { - border: 0; - clip: rect(0 0 0 0); - clip-path: rect(0 0 0 0); - height: auto; - margin: 0; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - white-space: nowrap; -} - -/* ── Completion Modal ─────────────────────────────── */ -.mdb-completion-modal { - padding: 8px 4px 4px; - min-width: 280px; -} - -.mdb-completion-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 20px; -} - -.mdb-completion-icon { - font-size: 1.6em; - line-height: 1; -} - -.mdb-completion-title { - margin: 0; - font-size: 1.15em; - font-weight: 600; - color: var(--text-normal); -} - -.mdb-completion-stats { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 24px; -} - -.mdb-completion-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 7px 0; - border-bottom: 1px solid var(--background-modifier-border); -} - -.mdb-completion-label { - color: var(--text-muted); - font-size: 0.92em; -} - -.mdb-completion-value { - font-weight: 600; - color: var(--text-normal); - font-size: 0.95em; -} - -.mdb-stat-success { color: var(--color-green); } -.mdb-stat-error { color: var(--color-red); } -.mdb-stat-skipped { color: var(--text-muted); } - -.mdb-completion-notes { - margin-bottom: 20px; - padding: 8px 12px; - background: var(--background-secondary); - border-radius: var(--radius-s); - font-size: 0.88em; - color: var(--text-muted); -} - -.mdb-completion-notes p { - margin: 0; -} - -.mdb-completion-footer { - display: flex; - justify-content: flex-end; - padding-top: 12px; -} From aff5fabf6ea489cdef9d79f99878f26e8ac8e6ef Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:28 +0300 Subject: [PATCH 35/60] Delete models directory --- models/ArtistModel.ts | 63 -------------------------- models/BoardGameModel.ts | 64 --------------------------- models/BookModel.ts | 64 --------------------------- models/ComicMangaModel.ts | 80 --------------------------------- models/GameModel.ts | 68 ---------------------------- models/MediaTypeModel.ts | 49 --------------------- models/MovieModel.ts | 88 ------------------------------------- models/MusicReleaseModel.ts | 69 ----------------------------- models/SeasonModel.ts | 80 --------------------------------- models/SeriesModel.ts | 86 ------------------------------------ models/SongModel.ts | 84 ----------------------------------- models/WikiModel.ts | 52 ---------------------- 12 files changed, 847 deletions(-) delete mode 100644 models/ArtistModel.ts delete mode 100644 models/BoardGameModel.ts delete mode 100644 models/BookModel.ts delete mode 100644 models/ComicMangaModel.ts delete mode 100644 models/GameModel.ts delete mode 100644 models/MediaTypeModel.ts delete mode 100644 models/MovieModel.ts delete mode 100644 models/MusicReleaseModel.ts delete mode 100644 models/SeasonModel.ts delete mode 100644 models/SeriesModel.ts delete mode 100644 models/SongModel.ts delete mode 100644 models/WikiModel.ts diff --git a/models/ArtistModel.ts b/models/ArtistModel.ts deleted file mode 100644 index 9683da38..00000000 --- a/models/ArtistModel.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type ArtistData = ModelToData; - -export class ArtistModel extends MediaTypeModel { - genres: string[]; - country: string; - image: string; - officialWebsite: string; - disambiguation: string; - /** ISNI(s) from the data source; comma-separated if multiple. */ - isni: string; - beginYear: string; - releaseDate: string; - - userData: { - personalRating: number; - }; - - constructor(obj: ArtistData) { - super(); - - this.genres = []; - this.country = ''; - this.image = ''; - this.officialWebsite = ''; - this.disambiguation = ''; - this.isni = ''; - this.beginYear = ''; - this.releaseDate = ''; - - this.userData = { - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - this.releaseDate = obj.releaseDate ?? ''; - } - - getTags(): string[] { - return [mediaDbTag, 'music', 'artist']; - } - - getMediaType(): MediaType { - return MediaType.Artist; - } - - getSummary(): string { - let summary = this.title; - if (this.beginYear) summary += ` (formed ${this.beginYear})`; - if (this.disambiguation) summary += ` — ${this.disambiguation}`; - return summary; - } -} diff --git a/models/BoardGameModel.ts b/models/BoardGameModel.ts deleted file mode 100644 index c65599b9..00000000 --- a/models/BoardGameModel.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type BoardGameData = ModelToData; - -export class BoardGameModel extends MediaTypeModel { - genres: string[]; - onlineRating: number; - complexityRating: number; - minPlayers: number; - maxPlayers: number; - playtime: string; - publishers: string[]; - image?: string; - - released: boolean; - - userData: { - played: boolean; - personalRating: number; - }; - - constructor(obj: BoardGameData) { - super(); - - this.genres = []; - this.onlineRating = 0; - this.complexityRating = 0; - this.minPlayers = 0; - this.maxPlayers = 0; - this.playtime = ''; - this.publishers = []; - this.image = ''; - - this.released = false; - - this.userData = { - played: false, - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'boardgame']; - } - - getMediaType(): MediaType { - return MediaType.BoardGame; - } - - getSummary(): string { - return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); - } -} diff --git a/models/BookModel.ts b/models/BookModel.ts deleted file mode 100644 index e2f52e48..00000000 --- a/models/BookModel.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type BookData = ModelToData; - -export class BookModel extends MediaTypeModel { - author: string; - plot: string; - pages: number; - image: string; - onlineRating: number; - isbn: number; - isbn13: number; - - released: boolean; - - userData: { - read: boolean; - lastRead: string; - personalRating: number; - }; - - constructor(obj: BookData) { - super(); - - this.author = ''; - this.plot = ''; - this.pages = 0; - this.image = ''; - this.onlineRating = 0; - this.isbn = 0; - this.isbn13 = 0; - - this.released = false; - - this.userData = { - read: false, - lastRead: '', - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'book']; - } - - getMediaType(): MediaType { - return MediaType.Book; - } - - getSummary(): string { - return this.englishTitle + (this.year > 0 ? ` (${this.year})` : '') + ' - ' + this.author; - } -} diff --git a/models/ComicMangaModel.ts b/models/ComicMangaModel.ts deleted file mode 100644 index 0d4c64a3..00000000 --- a/models/ComicMangaModel.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type ComicMangaData = ModelToData; - -export class ComicMangaModel extends MediaTypeModel { - plot: string; - alternateTitles: string[]; - genres: string[]; - authors: string[]; - chapters: number; - volumes: number; - onlineRating: number; - image: string; - - released: boolean; - status: string; - publishers: string[]; - publishedFrom: string; - publishedTo: string; - - userData: { - read: boolean; - lastRead: string; - personalRating: number; - }; - - constructor(obj: ComicMangaData) { - super(); - - this.plot = ''; - this.alternateTitles = []; - this.genres = []; - this.authors = []; - this.chapters = 0; - this.volumes = 0; - this.onlineRating = 0; - this.image = ''; - - this.released = false; - this.status = ''; - this.publishers = []; - this.publishedFrom = ''; - this.publishedTo = ''; - - this.userData = { - read: false, - lastRead: '', - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - const tags = [mediaDbTag]; - if (this.subType) { - tags.push(this.subType); - } else { - tags.push('comicManga'); - } - return tags; - } - - getMediaType(): MediaType { - return MediaType.ComicManga; - } - - getSummary(): string { - return this.title + (this.year > 0 ? ` (${this.year})` : ''); - } -} diff --git a/models/GameModel.ts b/models/GameModel.ts deleted file mode 100644 index 90b972a2..00000000 --- a/models/GameModel.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type GameData = ModelToData; - -export class GameModel extends MediaTypeModel { - developers: string[]; - publishers: string[]; - genres: string[]; - onlineRating: number; - image: string; - summary: string; - series: string[]; - gameModes: string[]; - platforms: string[]; - - released: boolean; - releaseDate: string; - - userData: { - played: boolean; - personalRating: number; - }; - - constructor(obj: GameData) { - super(); - - this.developers = []; - this.publishers = []; - this.genres = []; - this.onlineRating = 0; - this.image = ''; - this.summary = ''; - this.series = []; - this.gameModes = []; - this.platforms = []; - - this.released = false; - this.releaseDate = ''; - - this.userData = { - played: false, - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'game']; - } - - getMediaType(): MediaType { - return MediaType.Game; - } - - getSummary(): string { - return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); - } -} diff --git a/models/MediaTypeModel.ts b/models/MediaTypeModel.ts deleted file mode 100644 index e7e100b1..00000000 --- a/models/MediaTypeModel.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { MediaType } from '../utils/MediaType'; - -export abstract class MediaTypeModel { - type: string; - subType: string; - title: string; - englishTitle: string; - year: number; - dataSource: string; - url: string; - id: string; - image?: string; - - userData: object; - - protected constructor() { - this.type = ''; - this.subType = ''; - this.title = ''; - this.englishTitle = ''; - this.year = 0; - this.dataSource = ''; - this.url = ''; - this.id = ''; - this.image = ''; - - this.userData = {}; - } - - abstract getMediaType(): MediaType; - - //a string that contains enough info to disambiguate from similar media - abstract getSummary(): string; - - abstract getTags(): string[]; - - toMetaDataObject(): Record { - const obj: Record = { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; - // year: 0 means "unknown" — write null so YAML shows blank (None) instead of 0 - if (obj['year'] === 0) obj['year'] = null; - return obj; - } - - getWithOutUserData(): Record { - const copy = structuredClone(this) as Record; - delete copy.userData; - return copy; - } -} diff --git a/models/MovieModel.ts b/models/MovieModel.ts deleted file mode 100644 index 08b7a1b9..00000000 --- a/models/MovieModel.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { coerceMovieDurationMinutes, mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type MovieData = ModelToData; - -export class MovieModel extends MediaTypeModel { - japaneseTitle: string; - plot: string; - genres: string[]; - director: string[]; - writer: string[]; - studio: string[]; - /** Total runtime in minutes. */ - duration: number; - onlineRating: number; - actors: string[]; - image: string; - - released: boolean; - country: string[]; - language: string[]; - /** Production budget in USD (e.g. from TMDB). */ - budget: string; - /** Box-office gross (e.g. worldwide from TMDB; OMDb US figure when from IMDb). */ - revenue: string; - ageRating: string; - streamingServices: string[]; - premiere: string; - - userData: { - watched: boolean; - lastWatched: string; - personalRating: number; - }; - - constructor(obj: MovieData) { - super(); - - this.japaneseTitle = ''; - this.plot = ''; - this.genres = []; - this.director = []; - this.writer = []; - this.studio = []; - this.duration = 0; - this.onlineRating = 0; - this.actors = []; - this.image = ''; - - this.released = false; - this.country = []; - this.language = []; - this.budget = ''; - this.revenue = ''; - this.ageRating = ''; - this.streamingServices = []; - this.premiere = ''; - - this.userData = { - watched: false, - lastWatched: '', - personalRating: 0, - }; - - migrateObject(this, obj, this); - this.duration = coerceMovieDurationMinutes(this.duration as unknown); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'tv', 'movie']; - } - - getMediaType(): MediaType { - return MediaType.Movie; - } - - getSummary(): string { - return this.englishTitle + (this.year > 0 ? ` (${this.year})` : ''); - } -} diff --git a/models/MusicReleaseModel.ts b/models/MusicReleaseModel.ts deleted file mode 100644 index 3c3d6634..00000000 --- a/models/MusicReleaseModel.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type MusicReleaseData = ModelToData; - -export class MusicReleaseModel extends MediaTypeModel { - genres: string[]; - artists: string[]; - language: string; - image: string; - rating: number; - releaseDate: string; - albumDuration: string; - trackCount: number; - tracks: { - number: number; - title: string; - duration: string; - featuredArtists: string[]; - /** MusicBrainz recording MBID; used to resolve Spotify and other links. */ - recordingId?: string; - }[]; - - userData: { - personalRating: number; - }; - - constructor(obj: MusicReleaseData) { - super(); - - this.genres = []; - this.artists = []; - this.image = ''; - this.rating = 0; - this.releaseDate = ''; - - this.userData = { - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - this.albumDuration = obj.albumDuration ?? '0:00'; - this.trackCount = obj.trackCount ?? 0; - this.tracks = obj.tracks ?? []; - this.language = obj.language ?? ''; - } - - getTags(): string[] { - return [mediaDbTag, 'music', this.subType]; - } - - getMediaType(): MediaType { - return MediaType.MusicRelease; - } - - getSummary(): string { - let summary = this.title + (this.year > 0 ? ` (${this.year})` : ''); - if (this.artists.length > 0) summary += ' - ' + this.artists.join(', '); - return summary; - } -} diff --git a/models/SeasonModel.ts b/models/SeasonModel.ts deleted file mode 100644 index fc4e0a3a..00000000 --- a/models/SeasonModel.ts +++ /dev/null @@ -1,80 +0,0 @@ -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 (!Object.hasOwn(obj, '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 this.seasonNumber + ' seasons'; - } -} diff --git a/models/SeriesModel.ts b/models/SeriesModel.ts deleted file mode 100644 index 4ca1662b..00000000 --- a/models/SeriesModel.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type SeriesData = ModelToData; - -export class SeriesModel extends MediaTypeModel { - japaneseTitle: string; - plot: string; - genres: string[]; - writer: string[]; - studio: string[]; - episodes: number; - duration: string; - onlineRating: number; - actors: string[]; - image: string; - - released: boolean; - country: string[]; - language: string[]; - network: string[]; - ageRating: string; - streamingServices: string[]; - airing: boolean; - airedFrom: string; - airedTo: string; - - userData: { - watched: boolean; - lastWatched: string; - personalRating: number; - }; - - constructor(obj: SeriesData) { - super(); - - this.japaneseTitle = ''; - this.plot = ''; - this.genres = []; - this.writer = []; - this.studio = []; - this.episodes = 0; - this.duration = ''; - this.onlineRating = 0; - this.actors = []; - this.image = ''; - - this.released = false; - this.country = []; - this.language = []; - this.network = []; - this.ageRating = ''; - this.streamingServices = []; - this.airing = false; - this.airedFrom = ''; - this.airedTo = ''; - - this.userData = { - watched: false, - lastWatched: '', - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'tv', 'series']; - } - - getMediaType(): MediaType { - return MediaType.Series; - } - - getSummary(): string { - return this.title + (this.year > 0 ? ` (${this.year})` : ''); - } -} diff --git a/models/SongModel.ts b/models/SongModel.ts deleted file mode 100644 index 6c45c8aa..00000000 --- a/models/SongModel.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type SongData = ModelToData; - -export class SongModel extends MediaTypeModel { - genres: string[]; - artists: string[]; - albumTitle: string; - albumReleaseGroupId: string; - trackNumber: number; - duration: string; - featuredArtists: string[]; - geniusUrl: string; - /** Open track URL from MusicBrainz (e.g. https://open.spotify.com/track/…) when available. */ - spotifyUrl: string; - lyrics: string; - image: string; - releaseDate: string; - - userData: { - personalRating: number; - }; - - constructor(obj: SongData) { - super(); - - this.genres = []; - this.artists = []; - this.albumTitle = ''; - this.albumReleaseGroupId = ''; - this.trackNumber = 0; - this.duration = ''; - this.featuredArtists = []; - this.geniusUrl = ''; - this.spotifyUrl = ''; - this.lyrics = ''; - this.image = ''; - this.releaseDate = ''; - - this.userData = { - personalRating: 0, - }; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - this.trackNumber = obj.trackNumber ?? 0; - this.albumTitle = obj.albumTitle ?? ''; - this.albumReleaseGroupId = obj.albumReleaseGroupId ?? ''; - this.duration = obj.duration ?? ''; - this.featuredArtists = obj.featuredArtists ?? []; - this.geniusUrl = obj.geniusUrl ?? ''; - this.spotifyUrl = obj.spotifyUrl ?? ''; - this.lyrics = obj.lyrics ?? ''; - this.releaseDate = obj.releaseDate ?? ''; - } - - getTags(): string[] { - return [mediaDbTag, 'music', 'song']; - } - - getMediaType(): MediaType { - return MediaType.Song; - } - - getSummary(): string { - const albumPart = this.albumTitle ? ` — ${this.albumTitle}` : ''; - const artists = this.artists.length > 0 ? this.artists.join(', ') : ''; - return `${this.title}${albumPart}${artists ? ` (${artists})` : ''}`; - } - - getWithOutUserData(): Record { - const copy = super.getWithOutUserData(); - delete copy.lyrics; - return copy; - } -} diff --git a/models/WikiModel.ts b/models/WikiModel.ts deleted file mode 100644 index ee49c2dc..00000000 --- a/models/WikiModel.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MediaType } from '../utils/MediaType'; -import type { ModelToData } from '../utils/Utils'; -import { mediaDbTag, migrateObject } from '../utils/Utils'; -import { MediaTypeModel } from './MediaTypeModel'; - -export type WikiData = ModelToData; - -export class WikiModel extends MediaTypeModel { - wikiUrl: string; - lastUpdated: string; - length: number; - article: string; - - userData: Record; - - constructor(obj: WikiData) { - super(); - - this.wikiUrl = ''; - this.lastUpdated = ''; - this.length = 0; - this.article = ''; - this.userData = {}; - - migrateObject(this, obj, this); - - if (!Object.hasOwn(obj, 'userData')) { - migrateObject(this.userData, obj, this.userData); - } - - this.type = this.getMediaType(); - } - - getTags(): string[] { - return [mediaDbTag, 'wiki']; - } - - getMediaType(): MediaType { - return MediaType.Wiki; - } - - override getWithOutUserData(): Record { - const copy = structuredClone(this) as Record; - delete copy.userData; - delete copy.article; - return copy; - } - - getSummary(): string { - return this.title; - } -} From ac21a7ff0a65633d0d8f25a1b4ecd4c353694fbb Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:33 +0300 Subject: [PATCH 36/60] Delete modals directory --- modals/BulkUpdateConfirmModal.ts | 39 ------ modals/CompletionModal.ts | 85 ------------- modals/ConfirmOverwriteModal.ts | 45 ------- modals/MediaDbAdvancedSearchModal.ts | 133 -------------------- modals/MediaDbBulkImportModal.ts | 134 -------------------- modals/MediaDbIdSearchModal.ts | 119 ------------------ modals/MediaDbPreviewModal.ts | 86 ------------- modals/MediaDbSearchModal.ts | 145 ---------------------- modals/MediaDbSearchResultModal.ts | 66 ---------- modals/MediaDbSeasonSelectModal.ts | 50 -------- modals/PropertyMappingModal.ts | 59 --------- modals/SelectModal.ts | 176 --------------------------- modals/SelectModalElement.ts | 88 -------------- 13 files changed, 1225 deletions(-) delete mode 100644 modals/BulkUpdateConfirmModal.ts delete mode 100644 modals/CompletionModal.ts delete mode 100644 modals/ConfirmOverwriteModal.ts delete mode 100644 modals/MediaDbAdvancedSearchModal.ts delete mode 100644 modals/MediaDbBulkImportModal.ts delete mode 100644 modals/MediaDbIdSearchModal.ts delete mode 100644 modals/MediaDbPreviewModal.ts delete mode 100644 modals/MediaDbSearchModal.ts delete mode 100644 modals/MediaDbSearchResultModal.ts delete mode 100644 modals/MediaDbSeasonSelectModal.ts delete mode 100644 modals/PropertyMappingModal.ts delete mode 100644 modals/SelectModal.ts delete mode 100644 modals/SelectModalElement.ts diff --git a/modals/BulkUpdateConfirmModal.ts b/modals/BulkUpdateConfirmModal.ts deleted file mode 100644 index 5b9effe4..00000000 --- a/modals/BulkUpdateConfirmModal.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type App, Modal, Setting } from 'obsidian'; - -export class BulkUpdateConfirmModal extends Modal { - onSubmit: (silent: boolean) => void; - silentUpdate: boolean = true; - - constructor(app: App, onSubmit: (silent: boolean) => void) { - super(app); - this.onSubmit = onSubmit; - } - - onOpen() { - const { contentEl } = this; - contentEl.createEl('h2', { text: 'Bulk Update Metadata' }); - contentEl.createEl('p', { text: 'You are about to scan and update metadata for notes in this folder.' }); - - new Setting(contentEl) - .setName('Update Silently (No Confirmations)') - .setDesc('If enabled, all updates will aggressively overwrite the note frontmatter without asking for individual confirmation for each file.') - .addToggle(toggle => toggle - .setValue(this.silentUpdate) - .onChange(value => (this.silentUpdate = value)) - ); - - new Setting(contentEl) - .addButton(btn => btn - .setButtonText('Start Update') - .setCta() - .onClick(() => { - this.close(); - this.onSubmit(this.silentUpdate); - })); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} diff --git a/modals/CompletionModal.ts b/modals/CompletionModal.ts deleted file mode 100644 index 233c3008..00000000 --- a/modals/CompletionModal.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { type App, Modal, ButtonComponent } from 'obsidian'; - -export interface CompletionResult { - /** Title shown in the modal header */ - title: string; - /** Icon emoji for the operation type */ - icon?: string; - /** Total number of items processed */ - total: number; - /** Number of successfully processed items */ - success: number; - /** Number of failed items */ - errors: number; - /** Number of skipped items (optional) */ - skipped?: number; - /** Elapsed time in milliseconds */ - elapsedMs?: number; - /** Optional extra lines shown below the stats */ - notes?: string[]; -} - -export class CompletionModal extends Modal { - private result: CompletionResult; - - constructor(app: App, result: CompletionResult) { - super(app); - this.result = result; - } - - onOpen(): void { - const { contentEl } = this; - contentEl.empty(); - contentEl.addClass('mdb-completion-modal'); - - const r = this.result; - const icon = r.icon ?? '✅'; - const allSuccess = r.errors === 0; - - // Header - const header = contentEl.createEl('div', { cls: 'mdb-completion-header' }); - header.createEl('span', { cls: 'mdb-completion-icon', text: allSuccess ? icon : '⚠️' }); - header.createEl('h2', { cls: 'mdb-completion-title', text: r.title }); - - // Stats - const stats = contentEl.createEl('div', { cls: 'mdb-completion-stats' }); - - this.addStatRow(stats, '📄 Total', `${r.total}`); - this.addStatRow(stats, '✅ Successful', `${r.success}`, 'success'); - this.addStatRow(stats, '❌ Errors', `${r.errors}`, r.errors > 0 ? 'error' : undefined); - - if (r.skipped !== undefined) { - this.addStatRow(stats, '⏭️ Skipped', `${r.skipped}`, 'skipped'); - } - - if (r.elapsedMs !== undefined) { - const secs = (r.elapsedMs / 1000).toFixed(1); - this.addStatRow(stats, '⏱️ Duration', `${secs}s`); - } - - // Notes - if (r.notes && r.notes.length > 0) { - const notesEl = contentEl.createEl('div', { cls: 'mdb-completion-notes' }); - for (const note of r.notes) { - notesEl.createEl('p', { text: note }); - } - } - - // Close button - const footer = contentEl.createEl('div', { cls: 'mdb-completion-footer' }); - new ButtonComponent(footer) - .setButtonText('Close') - .setCta() - .onClick(() => this.close()); - } - - onClose(): void { - this.contentEl.empty(); - } - - private addStatRow(container: HTMLElement, label: string, value: string, cls?: string): void { - const row = container.createEl('div', { cls: 'mdb-completion-row' }); - row.createEl('span', { cls: 'mdb-completion-label', text: label }); - row.createEl('span', { cls: `mdb-completion-value${cls ? ' mdb-stat-' + cls : ''}`, text: value }); - } -} diff --git a/modals/ConfirmOverwriteModal.ts b/modals/ConfirmOverwriteModal.ts deleted file mode 100644 index 9852da26..00000000 --- a/modals/ConfirmOverwriteModal.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { App } from 'obsidian'; -import { Modal, Setting } from 'obsidian'; - -export class ConfirmOverwriteModal extends Modal { - result: boolean = false; - onSubmit: (result: boolean) => void; - fileName: string; - - constructor(app: App, fileName: string, onSubmit: (result: boolean) => void) { - super(app); - this.fileName = fileName; - this.onSubmit = onSubmit; - } - - onOpen(): void { - const { contentEl } = this; - contentEl.createEl('h2', { text: 'File already exists' }); - contentEl.createEl('p', { text: `The file "${this.fileName}" already exists. Do you want to overwrite it?` }); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - const bottomSettingRow = new Setting(contentEl); - - bottomSettingRow.addButton(btn => { - btn.setButtonText('No'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }); - bottomSettingRow.addButton(btn => { - btn.setButtonText('Yes'); - btn.setCta(); - btn.onClick(() => { - this.result = true; - this.close(); - }); - btn.buttonEl.addClass('media-db-plugin-button'); - }); - } - - onClose(): void { - const { contentEl } = this; - contentEl.empty(); - this.onSubmit(this.result); - } -} diff --git a/modals/MediaDbAdvancedSearchModal.ts b/modals/MediaDbAdvancedSearchModal.ts deleted file mode 100644 index e0e80207..00000000 --- a/modals/MediaDbAdvancedSearchModal.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { ButtonComponent } from 'obsidian'; -import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { AdvancedSearchModalData, AdvancedSearchModalOptions } from '../utils/ModalHelper'; -import { ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; - -export class MediaDbAdvancedSearchModal extends Modal { - plugin: MediaDbPlugin; - - query: string; - isBusy: boolean; - title: string; - selectedApis: string[]; - - searchBtn?: ButtonComponent; - - submitCallback?: (res: AdvancedSearchModalData) => void; - closeCallback?: (err?: Error) => void; - - constructor(plugin: MediaDbPlugin, advancedSearchModalOptions: AdvancedSearchModalOptions) { - advancedSearchModalOptions = Object.assign({}, ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, advancedSearchModalOptions); - super(plugin.app); - - this.plugin = plugin; - this.selectedApis = []; - this.title = advancedSearchModalOptions.modalTitle ?? ''; - this.query = advancedSearchModalOptions.prefilledSearchString ?? ''; - this.isBusy = false; - } - - setSubmitCb(submitCallback: (res: AdvancedSearchModalData) => void): void { - this.submitCallback = submitCallback; - } - - setCloseCb(closeCallback: (err?: Error) => void): void { - this.closeCallback = closeCallback; - } - - keyPressCallback(event: KeyboardEvent): void { - if (event.key === 'Enter') { - void this.search(); - } - } - - async search(): Promise { - if (!this.query || this.query.length < 3) { - new Notice('MDB | Query too short'); - return; - } - - const apis: string[] = this.selectedApis; - - if (apis.length === 0) { - new Notice('MDB | No API selected'); - return; - } - - if (!this.isBusy) { - this.isBusy = true; - this.searchBtn?.setDisabled(false); - this.searchBtn?.setButtonText('Searching...'); - - this.submitCallback?.({ query: this.query, apis: apis }); - } - } - - onOpen(): void { - const { contentEl } = this; - - contentEl.createEl('h2', { text: this.title }); - - const placeholder = 'Search by title'; - const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; - searchComponent.setPlaceholder(placeholder); - searchComponent.setValue(this.query); - searchComponent.onChange(value => (this.query = value)); - searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); - - contentEl.appendChild(searchComponent.inputEl); - searchComponent.inputEl.focus(); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - contentEl.createEl('h3', { text: 'APIs to search' }); - - // const apiToggleComponents: Component[] = []; - for (const api of this.plugin.apiManager.apis) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: api.apiName, cls: 'media-db-plugin-list-text' }); - apiToggleTextWrapper.createEl('small', { text: api.apiDescription, cls: 'media-db-plugin-list-text' }); - - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); - - const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); - apiToggleComponent.setTooltip(api.apiName); - apiToggleComponent.setValue(this.selectedApis.some(x => x === api.apiName)); - apiToggleComponent.onChange(value => { - if (value) { - this.selectedApis.push(api.apiName); - } else { - this.selectedApis = this.selectedApis.filter(x => x !== api.apiName); - } - }); - apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); - } - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - new Setting(contentEl) - .addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }) - .addButton(btn => { - btn.setButtonText('Ok'); - btn.setCta(); - btn.onClick(() => { - void this.search(); - }); - btn.buttonEl.addClass('media-db-plugin-button'); - this.searchBtn = btn; - }); - } - - onClose(): void { - this.closeCallback?.(); - const { contentEl } = this; - contentEl.empty(); - } -} diff --git a/modals/MediaDbBulkImportModal.ts b/modals/MediaDbBulkImportModal.ts deleted file mode 100644 index 1aee8eeb..00000000 --- a/modals/MediaDbBulkImportModal.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { ButtonComponent } from 'obsidian'; -import { DropdownComponent, Modal, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type { APIModel } from 'src/api/APIModel'; -import { BulkImportLookupMethod } from 'src/utils/BulkImportHelper'; -import type MediaDbPlugin from '../main'; - -export class MediaDbBulkImportModal extends Modal { - plugin: MediaDbPlugin; - onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void; - selectedApi: string; - searchBtn?: ButtonComponent; - lookupMethod: BulkImportLookupMethod; - fieldName: string; - appendContent: boolean; - - constructor(plugin: MediaDbPlugin, onSubmit: (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => void) { - super(plugin.app); - this.plugin = plugin; - this.onSubmit = onSubmit; - this.selectedApi = plugin.apiManager.apis[0].apiName; - this.lookupMethod = BulkImportLookupMethod.TITLE; - this.fieldName = ''; - this.appendContent = false; - } - - submit(): void { - this.onSubmit(this.selectedApi, this.lookupMethod, this.fieldName, this.appendContent); - this.close(); - } - - onOpen(): void { - const { contentEl } = this; - - contentEl.createEl('h2', { text: 'Import folder as Media DB entries' }); - - this.createDropdownEl( - contentEl, - 'API to search', - (value: string) => { - this.selectedApi = value; - }, - this.plugin.apiManager.apis.map((api: APIModel) => { - return { value: api.apiName, display: api.apiName }; - }), - ); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - contentEl.createEl('h3', { text: 'Append note content to Media DB entry?' }); - - const appendContentToggleElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const appendContentToggleTextWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - appendContentToggleTextWrapper.createEl('span', { - text: 'If this is enabled, the plugin will override metadata fields with the same name.', - cls: 'media-db-plugin-list-text', - }); - - const appendContentToggleComponentWrapper = appendContentToggleElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); - - const appendContentToggle = new ToggleComponent(appendContentToggleElementWrapper); - appendContentToggle.setValue(false); - appendContentToggle.onChange(value => (this.appendContent = value)); - appendContentToggleComponentWrapper.appendChild(appendContentToggle.toggleEl); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - contentEl.createEl('h3', { text: 'Media lookup method' }); - contentEl.createEl('p', { - text: 'Choose whether to search the API by title (can return multiple results) or lookup directly using an ID (returns at most one result), and specify the name of the frontmatter property which contains the title or ID of the media.', - }); - - this.createDropdownEl( - contentEl, - 'Lookup media by', - (value: string) => { - this.lookupMethod = value as BulkImportLookupMethod; - }, - [ - { value: BulkImportLookupMethod.TITLE, display: 'Title' }, - { value: BulkImportLookupMethod.ID, display: 'ID' }, - ], - ); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - const fieldNameWrapperEl = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const fieldNameLabelWrapperEl = fieldNameWrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - fieldNameLabelWrapperEl.createEl('span', { text: 'Using the property named', cls: 'media-db-plugin-list-text' }); - - const fieldNameComponent = new TextComponent(fieldNameWrapperEl); - fieldNameComponent.setPlaceholder('title / id'); - fieldNameComponent.onChange(value => (this.fieldName = value)); - fieldNameComponent.inputEl.addEventListener('keydown', ke => { - if (ke.key === 'Enter') { - this.submit(); - } - }); - contentEl.appendChild(fieldNameWrapperEl); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - new Setting(contentEl) - .addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }) - .addButton(btn => { - btn.setButtonText('Ok'); - btn.setCta(); - btn.onClick(() => { - this.submit(); - }); - btn.buttonEl.addClass('media-db-plugin-button'); - this.searchBtn = btn; - }); - } - - createDropdownEl(parentEl: HTMLElement, label: string, onChange: (value: string) => void, options: { value: string; display: string }[]): void { - const wrapperEl = parentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const labelWrapperEl = wrapperEl.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - labelWrapperEl.createEl('span', { text: label, cls: 'media-db-plugin-list-text' }); - - const dropDownComponent = new DropdownComponent(wrapperEl); - dropDownComponent.onChange(onChange); - for (const option of options) { - dropDownComponent.addOption(option.value, option.display); - } - wrapperEl.appendChild(dropDownComponent.selectEl); - } - - onClose(): void { - const { contentEl } = this; - contentEl.empty(); - } -} diff --git a/modals/MediaDbIdSearchModal.ts b/modals/MediaDbIdSearchModal.ts deleted file mode 100644 index 7027bb52..00000000 --- a/modals/MediaDbIdSearchModal.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { ButtonComponent } from 'obsidian'; -import { DropdownComponent, Modal, Notice, Setting, TextComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { IdSearchModalData, IdSearchModalOptions } from '../utils/ModalHelper'; -import { ID_SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; - -export class MediaDbIdSearchModal extends Modal { - plugin: MediaDbPlugin; - - query: string; - isBusy: boolean; - title: string; - selectedApi: string; - - searchBtn?: ButtonComponent; - - submitCallback?: (res: IdSearchModalData, err?: Error) => void; - closeCallback?: (err?: Error) => void; - - constructor(plugin: MediaDbPlugin, idSearchModalOptions: IdSearchModalOptions) { - idSearchModalOptions = Object.assign({}, ID_SEARCH_MODAL_DEFAULT_OPTIONS, idSearchModalOptions); - super(plugin.app); - - this.plugin = plugin; - this.title = idSearchModalOptions.modalTitle ?? ''; - this.selectedApi = idSearchModalOptions.preselectedAPI ?? plugin.apiManager.apis[0].apiName; - this.query = ''; - this.isBusy = false; - } - - setSubmitCb(submitCallback: (res: IdSearchModalData, err?: Error) => void): void { - this.submitCallback = submitCallback; - } - - setCloseCb(closeCallback: (err?: Error) => void): void { - this.closeCallback = closeCallback; - } - - keyPressCallback(event: KeyboardEvent): void { - if (event.key === 'Enter') { - void this.search(); - } - } - - async search(): Promise { - if (!this.query) { - new Notice('MDB | no Id entered'); - return; - } - - if (!this.selectedApi) { - new Notice('MDB | No API selected'); - return; - } - - if (!this.isBusy) { - this.isBusy = true; - this.searchBtn?.setDisabled(false); - this.searchBtn?.setButtonText('Searching...'); - - this.submitCallback?.({ query: this.query, api: this.selectedApi }); - } - } - - onOpen(): void { - const { contentEl } = this; - - contentEl.createEl('h2', { text: this.title }); - - const placeholder = 'Search by id'; - const searchComponent = new TextComponent(contentEl); - searchComponent.inputEl.style.width = '100%'; - searchComponent.setPlaceholder(placeholder); - searchComponent.onChange(value => (this.query = value)); - searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); - - contentEl.appendChild(searchComponent.inputEl); - searchComponent.inputEl.focus(); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - const apiSelectorWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - const apiSelectorTExtWrapper = apiSelectorWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiSelectorTExtWrapper.createEl('span', { text: 'API to search', cls: 'media-db-plugin-list-text' }); - - const apiSelectorComponent = new DropdownComponent(apiSelectorWrapper); - apiSelectorComponent.onChange((value: string) => { - this.selectedApi = value; - }); - for (const api of this.plugin.apiManager.apis) { - apiSelectorComponent.addOption(api.apiName, api.apiName); - } - apiSelectorWrapper.appendChild(apiSelectorComponent.selectEl); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - new Setting(contentEl) - .addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }) - .addButton(btn => { - btn.setButtonText('Ok'); - btn.setCta(); - btn.onClick(() => { - void this.search(); - }); - btn.buttonEl.addClass('media-db-plugin-button'); - this.searchBtn = btn; - }); - } - - onClose(): void { - this.closeCallback?.(); - const { contentEl } = this; - contentEl.empty(); - } -} diff --git a/modals/MediaDbPreviewModal.ts b/modals/MediaDbPreviewModal.ts deleted file mode 100644 index 931556c1..00000000 --- a/modals/MediaDbPreviewModal.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Component, MarkdownRenderer, Modal, Setting } from 'obsidian'; -import type MediaDbPlugin from 'src/main'; -import type { MediaTypeModel } from 'src/models/MediaTypeModel'; -import type { PreviewModalData, PreviewModalOptions } from '../utils/ModalHelper'; -import { PREVIEW_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; - -export class MediaDbPreviewModal extends Modal { - plugin: MediaDbPlugin; - - elements: MediaTypeModel[]; - title: string; - markdownComponent: Component; - - submitCallback?: (previewModalData: PreviewModalData) => void; - closeCallback?: (err?: Error) => void; - - constructor(plugin: MediaDbPlugin, previewModalOptions: PreviewModalOptions) { - previewModalOptions = Object.assign({}, PREVIEW_MODAL_DEFAULT_OPTIONS, previewModalOptions); - - super(plugin.app); - - this.plugin = plugin; - this.title = previewModalOptions.modalTitle ?? ''; - this.elements = previewModalOptions.elements ?? []; - - this.markdownComponent = new Component(); - } - - setSubmitCb(submitCallback: (previewModalData: PreviewModalData) => void): void { - this.submitCallback = submitCallback; - } - - setCloseCb(closeCallback: (err?: Error) => void): void { - this.closeCallback = closeCallback; - } - - async preview(): Promise { - const { contentEl } = this; - contentEl.addClass('media-db-plugin-preview-modal'); - - contentEl.createEl('h2', { text: this.title }); - - const previewWrapper = contentEl.createDiv({ cls: 'media-db-plugin-preview-wrapper' }); - - this.markdownComponent.load(); - - for (const result of this.elements) { - previewWrapper.createEl('h3', { text: result.englishTitle }); - const fileDiv = previewWrapper.createDiv({ cls: 'media-db-plugin-preview' }); - - let fileContent = this.plugin.generateMediaDbNoteFrontmatterPreview(result); - fileContent = `\`\`\`yaml\n${fileContent}\`\`\``; - - try { - // TODO: fix this not rendering the frontmatter any more - await MarkdownRenderer.render(this.app, fileContent, fileDiv, '', this.markdownComponent); - } catch (e) { - console.warn(`mdb | error during rendering of preview`, e); - } - } - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - const bottomSettingRow = new Setting(contentEl); - bottomSettingRow.addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }); - bottomSettingRow.addButton(btn => { - btn.setButtonText('Ok'); - btn.setCta(); - btn.onClick(() => this.submitCallback?.({ confirmed: true })); - btn.buttonEl.addClass('media-db-plugin-button'); - }); - } - - onOpen(): void { - void this.preview(); - } - - onClose(): void { - this.markdownComponent.unload(); - this.closeCallback?.(); - } -} diff --git a/modals/MediaDbSearchModal.ts b/modals/MediaDbSearchModal.ts deleted file mode 100644 index 8332f5ab..00000000 --- a/modals/MediaDbSearchModal.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { ButtonComponent } from 'obsidian'; -import { Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; -import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import type { SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; -import { SEARCH_MODAL_DEFAULT_OPTIONS } from '../utils/ModalHelper'; -import { mediaTypeDisplayName } from '../utils/Utils'; - -export class MediaDbSearchModal extends Modal { - plugin: MediaDbPlugin; - - query: string; - isBusy: boolean; - title: string; - selectedTypes: MediaType[]; - - searchBtn?: ButtonComponent; - - submitCallback?: (res: SearchModalData) => void; - closeCallback?: (err?: Error) => void; - - constructor(plugin: MediaDbPlugin, searchModalOptions: SearchModalOptions) { - searchModalOptions = Object.assign({}, SEARCH_MODAL_DEFAULT_OPTIONS, searchModalOptions); - super(plugin.app); - - this.plugin = plugin; - this.selectedTypes = [...(searchModalOptions.preselectedTypes ?? [])]; - this.title = searchModalOptions.modalTitle ?? ''; - this.query = searchModalOptions.prefilledSearchString ?? ''; - this.isBusy = false; - } - - setSubmitCb(submitCallback: (res: SearchModalData) => void): void { - this.submitCallback = submitCallback; - } - - setCloseCb(closeCallback: (err?: Error) => void): void { - this.closeCallback = closeCallback; - } - - keyPressCallback(event: KeyboardEvent): void { - if (event.key === 'Enter') { - void this.search(); - } - } - - async search(): Promise { - if (!this.query || this.query.length < 3) { - new Notice('MDB | Query too short'); - return; - } - - const types: MediaType[] = this.selectedTypes; - - if (types.length === 0) { - new Notice('MDB | No Type selected'); - return; - } - - if (!this.isBusy) { - this.isBusy = true; - this.searchBtn?.setDisabled(false); - this.searchBtn?.setButtonText('Searching...'); - - this.submitCallback?.({ query: this.query, types: types }); - } - } - - onOpen(): void { - const { contentEl } = this; - - contentEl.createEl('h2', { text: this.title }); - - const placeholder = 'Search by title'; - const searchComponent = new TextComponent(contentEl); - let currentToggle: ToggleComponent | undefined = undefined; - - searchComponent.inputEl.style.width = '100%'; - searchComponent.setPlaceholder(placeholder); - searchComponent.setValue(this.query); - searchComponent.onChange(value => (this.query = value)); - searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this)); - - contentEl.appendChild(searchComponent.inputEl); - searchComponent.inputEl.focus(); - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - contentEl.createEl('h3', { text: 'APIs to search' }); - - for (const mediaType of MEDIA_TYPES) { - const apiToggleListElementWrapper = contentEl.createEl('div', { cls: 'media-db-plugin-list-wrapper' }); - - const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-text-wrapper' }); - apiToggleTextWrapper.createEl('span', { text: mediaTypeDisplayName(mediaType), cls: 'media-db-plugin-list-text' }); - - const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', { cls: 'media-db-plugin-list-toggle' }); - - const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper); - apiToggleComponent.setTooltip(mediaTypeDisplayName(mediaType)); - apiToggleComponent.setValue(this.selectedTypes.contains(mediaType)); - if (apiToggleComponent.getValue()) { - currentToggle = apiToggleComponent; - } - apiToggleComponent.onChange(value => { - if (value) { - if (currentToggle && currentToggle !== apiToggleComponent) { - currentToggle.setValue(false); - this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); - } - currentToggle = apiToggleComponent; - this.selectedTypes.push(mediaType); - } else { - currentToggle = undefined; - this.selectedTypes = this.selectedTypes.filter(x => x !== mediaType); - } - }); - apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl); - } - - contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); - - new Setting(contentEl) - .addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }) - .addButton(btn => { - btn.setButtonText('Ok'); - btn.setCta(); - btn.onClick(() => { - void this.search(); - }); - btn.buttonEl.addClass('media-db-plugin-button'); - this.searchBtn = btn; - }); - } - - onClose(): void { - this.closeCallback?.(); - const { contentEl } = this; - contentEl.empty(); - } -} diff --git a/modals/MediaDbSearchResultModal.ts b/modals/MediaDbSearchResultModal.ts deleted file mode 100644 index 786df40a..00000000 --- a/modals/MediaDbSearchResultModal.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { SelectModalData, SelectModalOptions } from '../utils/ModalHelper'; -import { SELECTMODALOPTIONSDEFAULT } from '../utils/ModalHelper'; -import { SelectModal } from './SelectModal'; - -export class MediaDbSearchResultModal extends SelectModal { - plugin: MediaDbPlugin; - - busy: boolean; - sendCallback: boolean; - - submitCallback?: (res: SelectModalData) => void; - closeCallback?: (err?: Error) => void; - skipCallback?: () => void; - submitButtonText: string; - - constructor(plugin: MediaDbPlugin, selectModalOptions: SelectModalOptions) { - selectModalOptions = Object.assign({}, SELECTMODALOPTIONSDEFAULT, selectModalOptions); - super(plugin.app, selectModalOptions.elements ?? [], selectModalOptions.multiSelect); - this.plugin = plugin; - this.title = selectModalOptions.modalTitle ?? ''; - this.description = selectModalOptions.description ?? 'Select one or multiple search results.'; - this.addSkipButton = selectModalOptions.skipButton ?? false; - this.submitButtonText = selectModalOptions.submitButtonText ?? 'Ok'; - this.busy = false; - this.sendCallback = false; - } - - setSubmitCb(submitCallback: (res: SelectModalData) => void): void { - this.submitCallback = submitCallback; - } - - setCloseCb(closeCallback: (err?: Error) => void): void { - this.closeCallback = closeCallback; - } - - setSkipCallback(skipCallback: () => void): void { - this.skipCallback = skipCallback; - } - - // Renders each suggestion item. - renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); - el.createEl('small', { text: `${item.getSummary()}\n` }); - el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); - } - - // Perform action on the selected suggestion. - submit(): void { - if (!this.busy) { - this.busy = true; - this.submitButton?.setButtonText('Creating entry...'); - this.submitCallback?.({ selected: this.selectModalElements.filter(x => x.isActive()).map(x => x.value) }); - } - } - - skip(): void { - this.skipButton?.setButtonText('Skipping...'); - this.skipCallback?.(); - } - - onClose(): void { - this.closeCallback?.(); - } -} diff --git a/modals/MediaDbSeasonSelectModal.ts b/modals/MediaDbSeasonSelectModal.ts deleted file mode 100644 index 0066c31e..00000000 --- a/modals/MediaDbSeasonSelectModal.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type MediaDbPlugin from '../main'; -import { SelectModal } from './SelectModal'; - -export interface SeasonSelectModalElement { - season_number: number; - name: string; - air_date?: string; - poster_path?: string; -} - -export class MediaDbSeasonSelectModal extends SelectModal { - plugin: MediaDbPlugin; - submitCallback?: (selectedSeasons: SeasonSelectModalElement[]) => void; - closeCallback?: (err?: Error) => void; - seriesName?: string; - - constructor(plugin: MediaDbPlugin, seasons: SeasonSelectModalElement[], multiSelect = true, seriesName?: string) { - super(plugin.app, seasons, multiSelect); - this.plugin = plugin; - this.seriesName = seriesName; - this.title = `Select seasons for${seriesName ? ` ${seriesName}` : ''}`; - this.description = 'Select one or more seasons to create notes for.'; - this.submitButtonText = 'Create Entry'; - } - - renderElement(season: SeasonSelectModalElement, el: HTMLElement): void { - el.createEl('div', { text: `${season.name}` }); - if (season.air_date) { - el.createEl('small', { text: `Air date: ${season.air_date}` }); - } - } - - submit(): void { - const selected = this.selectModalElements.filter(x => x.isActive()).map(x => x.value); - this.submitCallback?.(selected); - this.close(); - } - - skip(): void { - this.close(); - } - - setSubmitCb(cb: (selectedSeasons: SeasonSelectModalElement[]) => void): void { - this.submitCallback = cb; - } - - setCloseCb(cb: (err?: Error) => void): void { - this.closeCallback = cb; - } -} diff --git a/modals/PropertyMappingModal.ts b/modals/PropertyMappingModal.ts deleted file mode 100644 index 8f5adccd..00000000 --- a/modals/PropertyMappingModal.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { App } from 'obsidian'; -import { Modal } from 'obsidian'; -import { render } from 'solid-js/web'; -import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { mediaTypeDisplayName } from '../utils/Utils'; -import type { PropertyMappingModelData } from '../settings/PropertyMapping'; -import PropertyMappingModelComponent from '../settings/PropertyMappingModelComponent'; - -export class PropertyMappingModal extends Modal { - private disposeSolid?: () => void; - - constructor( - app: App, - private readonly plugin: MediaDbPlugin, - private readonly mediaType: MediaType, - ) { - super(app); - } - - onOpen(): void { - const { contentEl } = this; - this.setTitle(`Property mappings — ${mediaTypeDisplayName(this.mediaType)}`); - - const modelData = this.plugin.settings.propertyMappingModels.find(m => m.type === this.mediaType); - if (!modelData) { - contentEl.createEl('p', { text: 'No property mapping model found for this media type.' }); - return; - } - - contentEl.createEl('p', { - cls: 'mod-muted', - text: 'Choose whether each metadata field stays as-is, is renamed in front matter, or is omitted. Changes are saved automatically when valid.', - }); - - const root = contentEl.createDiv(); - this.disposeSolid = render( - () => - PropertyMappingModelComponent({ - model: structuredClone(modelData), - showMediaTypeTitle: false, - save: (model: PropertyMappingModelData): void => { - const index = this.plugin.settings.propertyMappingModels.findIndex(m => m.type === model.type); - if (index !== -1) { - this.plugin.settings.propertyMappingModels[index] = model; - } - void this.plugin.saveSettings(); - }, - }), - root, - ); - } - - onClose(): void { - this.disposeSolid?.(); - this.disposeSolid = undefined; - this.contentEl.empty(); - } -} diff --git a/modals/SelectModal.ts b/modals/SelectModal.ts deleted file mode 100644 index d54b5f99..00000000 --- a/modals/SelectModal.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { App, ButtonComponent } from 'obsidian'; -import { Modal, Setting } from 'obsidian'; -import { mod } from '../utils/Utils'; -import { SelectModalElement } from './SelectModalElement'; - -export abstract class SelectModal extends Modal { - allowMultiSelect: boolean; - - title: string; - description: string; - addSkipButton: boolean; - cancelButton?: ButtonComponent; - skipButton?: ButtonComponent; - submitButton?: ButtonComponent; - submitButtonText: string; - - elementWrapper?: HTMLDivElement; - - elements: T[]; - selectModalElements: SelectModalElement[]; - - protected constructor(app: App, elements: T[], allowMultiSelect: boolean = true) { - super(app); - this.allowMultiSelect = allowMultiSelect; - - this.title = ''; - this.description = ''; - this.addSkipButton = false; - this.submitButtonText = 'Ok'; - this.cancelButton = undefined; - this.skipButton = undefined; - this.submitButton = undefined; - - this.elementWrapper = undefined; - - this.elements = elements; - this.selectModalElements = []; - - this.scope.register([], 'ArrowUp', evt => { - this.highlightUp(); - evt.preventDefault(); - }); - this.scope.register([], 'ArrowDown', evt => { - this.highlightDown(); - evt.preventDefault(); - }); - this.scope.register([], 'ArrowRight', () => { - this.activateHighlighted(); - }); - this.scope.register([], ' ', evt => { - if (this.elementWrapper && this.elementWrapper === document.activeElement) { - this.activateHighlighted(); - evt.preventDefault(); - } - }); - this.scope.register([], 'Enter', () => this.submit()); - } - - abstract renderElement(value: T, el: HTMLElement): void; - - abstract submit(): void; - - abstract skip(): void; - - disableAllOtherElements(elementId: number): void { - for (const selectModalElement of this.selectModalElements) { - if (selectModalElement.id !== elementId) { - selectModalElement.setActive(false); - } - } - } - - deHighlightAllOtherElements(elementId: number): void { - for (const selectModalElement of this.selectModalElements) { - if (selectModalElement.id !== elementId) { - selectModalElement.setHighlighted(false); - } - } - } - - onOpen(): void { - const { contentEl, titleEl } = this; - - titleEl.createEl('h2', { text: this.title }); - contentEl.addClass('media-db-plugin-select-modal'); - contentEl.createEl('p', { text: this.description }); - - this.elementWrapper = contentEl.createDiv({ cls: 'media-db-plugin-select-wrapper' }); - this.elementWrapper.tabIndex = 0; - - let i = 0; - for (const element of this.elements) { - const selectModalElement = new SelectModalElement(element, this.elementWrapper, i, this, false); - - this.selectModalElements.push(selectModalElement); - - this.renderElement(element, selectModalElement.element); - - i += 1; - } - - this.selectModalElements.first()?.element.scrollIntoView(); - - const bottomSettingRow = new Setting(contentEl); - bottomSettingRow.addButton(btn => { - btn.setButtonText('Cancel'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - this.cancelButton = btn; - }); - if (this.addSkipButton) { - bottomSettingRow.addButton(btn => { - btn.setButtonText('Skip'); - btn.onClick(() => this.skip()); - btn.buttonEl.addClass('media-db-plugin-button'); - this.skipButton = btn; - }); - } - bottomSettingRow.addButton(btn => { - btn.setButtonText(this.submitButtonText); - btn.setCta(); - btn.onClick(() => this.submit()); - btn.buttonEl.addClass('media-db-plugin-button'); - this.submitButton = btn; - }); - } - - activateHighlighted(): void { - for (const selectModalElement of this.selectModalElements) { - if (selectModalElement.isHighlighted()) { - selectModalElement.setActive(!selectModalElement.isActive()); - if (!this.allowMultiSelect) { - this.disableAllOtherElements(selectModalElement.id); - } - } - } - } - - highlightUp(): void { - for (const selectModalElement of this.selectModalElements) { - if (selectModalElement.isHighlighted()) { - this.getPreviousSelectModalElement(selectModalElement).setHighlighted(true); - return; - } - } - - // nothing is highlighted - this.selectModalElements.last()?.setHighlighted(true); - } - - highlightDown(): void { - for (const selectModalElement of this.selectModalElements) { - if (selectModalElement.isHighlighted()) { - this.getNextSelectModalElement(selectModalElement).setHighlighted(true); - return; - } - } - - // nothing is highlighted - this.selectModalElements.first()?.setHighlighted(true); - } - - private getNextSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { - let nextId = selectModalElement.id + 1; - nextId = mod(nextId, this.selectModalElements.length); - - return this.selectModalElements.find(x => x.id === nextId)!; - } - - private getPreviousSelectModalElement(selectModalElement: SelectModalElement): SelectModalElement { - let nextId = selectModalElement.id - 1; - nextId = mod(nextId, this.selectModalElements.length); - - return this.selectModalElements.find(x => x.id === nextId)!; - } -} diff --git a/modals/SelectModalElement.ts b/modals/SelectModalElement.ts deleted file mode 100644 index 70c09f17..00000000 --- a/modals/SelectModalElement.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { SelectModal } from './SelectModal'; - -export class SelectModalElement { - selectModal: SelectModal; - value: T; - readonly id: number; - element: HTMLDivElement; - cssClass: string; - activeClass: string; - hoverClass: string; - private active: boolean; - private highlighted: boolean; - - constructor(value: T, parentElement: HTMLElement, id: number, selectModal: SelectModal, active: boolean = false) { - this.value = value; - this.id = id; - this.active = active; - this.selectModal = selectModal; - - this.cssClass = 'media-db-plugin-select-element'; - this.activeClass = 'media-db-plugin-select-element-selected'; - this.hoverClass = 'media-db-plugin-select-element-hover'; - - this.element = parentElement.createDiv({ cls: this.cssClass }); - this.element.id = this.getHTMLId(); - this.element.on('click', '#' + this.getHTMLId(), () => { - this.setActive(!this.active); - if (!this.selectModal.allowMultiSelect) { - this.selectModal.disableAllOtherElements(this.id); - } - }); - this.element.on('mouseenter', '#' + this.getHTMLId(), () => { - this.setHighlighted(true); - }); - this.element.on('mouseleave', '#' + this.getHTMLId(), () => { - this.setHighlighted(false); - }); - - this.highlighted = false; - } - - getHTMLId(): string { - return `media-db-plugin-select-element-${this.id}`; - } - - isHighlighted(): boolean { - return this.highlighted; - } - - setHighlighted(value: boolean): void { - this.highlighted = value; - if (this.highlighted) { - this.addClass(this.hoverClass); - this.selectModal.deHighlightAllOtherElements(this.id); - } else { - this.removeClass(this.hoverClass); - } - } - - isActive(): boolean { - return this.active; - } - - setActive(active: boolean): void { - this.active = active; - this.update(); - } - - update(): void { - if (this.active) { - this.addClass(this.activeClass); - } else { - this.removeClass(this.activeClass); - } - } - - addClass(cssClass: string): void { - if (!this.element.hasClass(cssClass)) { - this.element.addClass(cssClass); - } - } - - removeClass(cssClass: string): void { - if (this.element.hasClass(cssClass)) { - this.element.removeClass(cssClass); - } - } -} From ceb07d1e18124fd775d019dc15a81edfc3333043 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:39 +0300 Subject: [PATCH 37/60] Delete utils directory --- utils/AutoTrackerHelper.ts | 90 -- utils/BulkImportHelper.ts | 169 ---- utils/BulkUpdateHelper.ts | 64 -- utils/DateFormatter.ts | 65 -- utils/IconList.ts | 1185 ------------------------ utils/IllegalFilenameCharactersList.ts | 14 - utils/MediaType.ts | 13 - utils/MediaTypeManager.ts | 179 ---- utils/ModalHelper.ts | 537 ----------- utils/Utils.ts | 387 -------- utils/normalizeTitleForAlias.ts | 52 -- utils/noteTypeSettings.ts | 63 -- 12 files changed, 2818 deletions(-) delete mode 100644 utils/AutoTrackerHelper.ts delete mode 100644 utils/BulkImportHelper.ts delete mode 100644 utils/BulkUpdateHelper.ts delete mode 100644 utils/DateFormatter.ts delete mode 100644 utils/IconList.ts delete mode 100644 utils/IllegalFilenameCharactersList.ts delete mode 100644 utils/MediaType.ts delete mode 100644 utils/MediaTypeManager.ts delete mode 100644 utils/ModalHelper.ts delete mode 100644 utils/Utils.ts delete mode 100644 utils/normalizeTitleForAlias.ts delete mode 100644 utils/noteTypeSettings.ts diff --git a/utils/AutoTrackerHelper.ts b/utils/AutoTrackerHelper.ts deleted file mode 100644 index 7155c9dd..00000000 --- a/utils/AutoTrackerHelper.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Notice, TFile, TFolder } from 'obsidian'; -import type MediaDbPlugin from 'src/main'; -import { CompletionModal } from 'src/modals/CompletionModal'; -import { dateTimeToString, markdownTable } from './Utils'; - -export class AutoTrackerHelper { - readonly plugin: MediaDbPlugin; - public isScanning: boolean = false; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - async startBackgroundScan(silent: boolean = false, targetFolder?: TFolder): Promise { - if (this.isScanning) return; - this.isScanning = true; - this.plugin.refreshAutoTrackerRibbon(); - await this.runAutoUpdate(silent, targetFolder); - this.isScanning = false; - this.plugin.refreshAutoTrackerRibbon(); - } - - async runAutoUpdate(silent: boolean = false, targetFolder?: TFolder): Promise { - const allFiles = targetFolder - ? this.plugin.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(targetFolder.path)) - : this.plugin.app.vault.getMarkdownFiles(); - - const filesToUpdate: TFile[] = []; - - for (const file of allFiles) { - const metadata = this.plugin.getMetadataFromFileCache(file); - if (metadata && metadata.dataSource && metadata.id) { - const airingKey = this.plugin.settings.autoTrackerAiringKey; - const releasedKey = this.plugin.settings.autoTrackerReleasedKey; - if (metadata[airingKey] === true || metadata[releasedKey] === false) { - filesToUpdate.push(file); - } - } - } - - if (filesToUpdate.length === 0) { - if (!silent) { - new Notice('MDB Tracker | No airing or unreleased media found to update.'); - } - return; - } - - const noticeMsg = `MDB Tracker | Found ${filesToUpdate.length} ongoing/unreleased notes. Updating in background...`; - if (!silent) { - new Notice(noticeMsg); - } - console.log(noticeMsg); - - const startTime = Date.now(); - let successCount = 0; - let failCount = 0; - const erroredFiles: { filePath: string, error: string }[] = []; - - for (const file of filesToUpdate) { - try { - await this.plugin.updateNote(file, true, false, silent); - successCount++; - } catch (e) { - console.warn(`MDB Tracker | Failed to auto-update ${file.path}: `, e); - failCount++; - erroredFiles.push({ filePath: file.path, error: `${e}` }); - } - // Sleep longer (1s) to be completely safe during background checks - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - if (failCount > 0 && erroredFiles.length > 0) { - const title = `MDB - auto tracker error report ${dateTimeToString(new Date())}`; - const filePath = `${title}.md`; - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - const fileContent = markdownTable(table); - await this.plugin.app.vault.create(filePath, fileContent); - } - - new CompletionModal(this.plugin.app, { - title: 'Auto Tracker Complete', - icon: '🎯', - total: filesToUpdate.length, - success: successCount, - errors: failCount, - elapsedMs: Date.now() - startTime, - notes: failCount > 0 ? ['Some notes could not be updated. A detailed report file has been created in your vault folder.'] : [], - }).open(); - } -} diff --git a/utils/BulkImportHelper.ts b/utils/BulkImportHelper.ts deleted file mode 100644 index ffbe88dd..00000000 --- a/utils/BulkImportHelper.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { TFolder } from 'obsidian'; -import { TFile } from 'obsidian'; -import type MediaDbPlugin from 'src/main'; -import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; -import type { MediaTypeModel } from 'src/models/MediaTypeModel'; -import { CompletionModal } from 'src/modals/CompletionModal'; -import { ModalResultCode } from './ModalHelper'; -import { dateTimeToString, markdownTable } from './Utils'; - -export enum BulkImportLookupMethod { - ID = 'id', - TITLE = 'title', -} - -interface BulkImportError { - filePath: string; - error: string; - canceled?: boolean; -} - -export class BulkImportHelper { - readonly plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - async import(folder: TFolder): Promise { - const erroredFiles: BulkImportError[] = []; - let canceled: boolean = false; - let successCount = 0; - const startTime = Date.now(); - - const { selectedAPI, lookupMethod, fieldName, appendContent } = await new Promise<{ - selectedAPI: string; - lookupMethod: BulkImportLookupMethod; - fieldName: string; - appendContent: boolean; - }>(resolve => { - new MediaDbBulkImportModal(this.plugin, (selectedAPI: string, lookupMethod: BulkImportLookupMethod, fieldName: string, appendContent: boolean) => { - resolve({ selectedAPI, lookupMethod, fieldName, appendContent }); - }).open(); - }); - - for (const child of folder.children) { - if (!(child instanceof TFile)) { - continue; - } - - const file: TFile = child; - if (canceled) { - erroredFiles.push({ filePath: file.path, error: 'user canceled' }); - continue; - } - - const metadata = this.plugin.getMetadataFromFileCache(file); - const lookupValue = metadata[fieldName]; - - if (!lookupValue || typeof lookupValue !== 'string') { - erroredFiles.push({ filePath: file.path, error: `metadata field '${fieldName}' not found, empty, or not a string` }); - continue; - } else if (lookupMethod === BulkImportLookupMethod.ID) { - const error = await this.importById(file, lookupValue, selectedAPI, appendContent); - if (error) { - erroredFiles.push(error); - } else { - successCount++; - } - } else if (lookupMethod === BulkImportLookupMethod.TITLE) { - const error = await this.importByTitle(file, lookupValue, selectedAPI, appendContent); - if (error) { - if (error.canceled) { - canceled = true; - } - erroredFiles.push(error); - } else { - successCount++; - } - } else { - erroredFiles.push({ filePath: file.path, error: `invalid lookup type` }); - continue; - } - } - - if (erroredFiles.length > 0) { - await this.createErroredFilesReport(erroredFiles); - } - - const total = successCount + erroredFiles.length; - new CompletionModal(this.plugin.app, { - title: 'Bulk Import Complete', - icon: '📥', - total, - success: successCount, - errors: erroredFiles.filter(e => !e.canceled).length, - skipped: erroredFiles.filter(e => e.canceled).length, - elapsedMs: Date.now() - startTime, - notes: erroredFiles.length > 0 ? ['Error report saved to vault.'] : [], - }).open(); - } - - private async importById(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - try { - const model = await this.plugin.apiManager.queryDetailedInfoById(lookupValue, selectedAPI); - if (model) { - await this.plugin.createMediaDbNotes([model], appendContent ? file : undefined); - return undefined; - } else { - return { filePath: file.path, error: `Failed to query API with id: ${lookupValue}` }; - } - } catch (e) { - return { filePath: file.path, error: `${e}` }; - } - } - - private async importByTitle(file: TFile, lookupValue: string, selectedAPI: string, appendContent: boolean): Promise { - let results: MediaTypeModel[] = []; - try { - results = await this.plugin.apiManager.query(lookupValue, [selectedAPI]); - } catch (e) { - return { filePath: file.path, error: `${e}` }; - } - if (!results || results.length === 0) { - return { filePath: file.path, error: `no search results` }; - } - - const { selectModalResult, selectModal } = await this.plugin.modalHelper.createSelectModal({ - elements: results, - skipButton: true, - modalTitle: `Results for '${lookupValue}'`, - }); - - if (selectModalResult.code === ModalResultCode.ERROR) { - selectModal.close(); - return { filePath: file.path, error: selectModalResult.error.message }; - } - - if (selectModalResult.code === ModalResultCode.CLOSE) { - selectModal.close(); - return { filePath: file.path, error: 'user canceled', canceled: true }; - } - - if (selectModalResult.code === ModalResultCode.SKIP) { - selectModal.close(); - return { filePath: file.path, error: 'user skipped' }; - } - - if (selectModalResult.data.selected.length === 0) { - selectModal.close(); - return { filePath: file.path, error: `no search results selected` }; - } - - const detailedResults = await this.plugin.queryDetails(selectModalResult.data.selected); - await this.plugin.createMediaDbNotes(detailedResults, appendContent ? file : undefined); - - selectModal.close(); - return undefined; - } - - private async createErroredFilesReport(erroredFiles: BulkImportError[]): Promise { - const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`; - const filePath = `${title}.md`; - - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - - const fileContent = markdownTable(table); - await this.plugin.app.vault.create(filePath, fileContent); - } -} diff --git a/utils/BulkUpdateHelper.ts b/utils/BulkUpdateHelper.ts deleted file mode 100644 index d5c37db3..00000000 --- a/utils/BulkUpdateHelper.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { TFolder, TFile, Notice } from 'obsidian'; -import type MediaDbPlugin from 'src/main'; -import { BulkUpdateConfirmModal } from 'src/modals/BulkUpdateConfirmModal'; -import { CompletionModal } from 'src/modals/CompletionModal'; -import { dateTimeToString, markdownTable } from './Utils'; - -export class BulkUpdateHelper { - readonly plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - async updateFolder(folder: TFolder): Promise { - const mediaFiles = folder.children.filter((child): child is TFile => { - if (!(child instanceof TFile)) return false; - const metadata = this.plugin.getMetadataFromFileCache(child); - return Boolean(metadata && metadata.dataSource && metadata.id); - }); - - if (mediaFiles.length === 0) { - new Notice('MDB | No Media DB files found in this folder.'); - return; - } - - new BulkUpdateConfirmModal(this.plugin.app, async (silent: boolean) => { - new Notice(`MDB | Bulk updating ${mediaFiles.length} files. Please wait...`); - const startTime = Date.now(); - let successCount = 0; - let failCount = 0; - const erroredFiles: { filePath: string, error: string }[] = []; - - for (const file of mediaFiles) { - try { - await this.plugin.updateNote(file, true, false, silent); - successCount++; - } catch (e) { - console.error(`MDB | Failed to bulk update ${file.path}: `, e); - failCount++; - erroredFiles.push({ filePath: file.path, error: `${e}` }); - } - await new Promise(resolve => setTimeout(resolve, 800)); - } - - if (failCount > 0 && erroredFiles.length > 0) { - const title = `MDB - bulk update error report ${dateTimeToString(new Date())}`; - const filePath = `${title}.md`; - const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - const fileContent = markdownTable(table); - await this.plugin.app.vault.create(filePath, fileContent); - } - - new CompletionModal(this.plugin.app, { - title: 'Bulk Update Complete', - icon: '🔄', - total: mediaFiles.length, - success: successCount, - errors: failCount, - elapsedMs: Date.now() - startTime, - notes: failCount > 0 ? ['Some files could not be updated. A detailed report file has been created in your vault folder.'] : [], - }).open(); - }).open(); - } -} diff --git a/utils/DateFormatter.ts b/utils/DateFormatter.ts deleted file mode 100644 index 0088af06..00000000 --- a/utils/DateFormatter.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { moment } from 'obsidian'; - -export class DateFormatter { - toFormat: string; - locale: string; - - constructor() { - this.toFormat = 'YYYY-MM-DD'; - // get locale of this machine (e.g. en, en-gb, de, fr, etc.) - this.locale = new Intl.DateTimeFormat().resolvedOptions().locale; - } - - setFormat(format: string): void { - this.toFormat = format; - } - - getPreview(format?: string): string { - const today = moment(); - - format ??= this.toFormat; - - return today.locale(this.locale).format(format); - } - - /** - * Tries to format a given date string with the currently set date format. - * You can set a date format by calling `setFormat()`. - * - * @param dateString the date string to be formatted - * @param dateFormat the current format of `dateString`. When this is `null` and the actual format of the - * given date string is not `C2822` or `ISO` format, this function will try to guess the format by using the native `Date` module. - * @param locale the locale of `dateString`. This is needed when `dateString` includes a month or day name and its locale format differs - * from the locale of this machine. - * @returns formatted date string or null if `dateString` is not a valid date - */ - format(dateString: string | null | undefined, dateFormat?: string, locale: string = 'en'): string | null { - if (!dateString) { - return null; - } - - let date: moment.Moment; - - if (!dateFormat) { - // reading date formats other then C2822 or ISO with moment is deprecated - // see https://momentjs.com/docs/#/parsing/string/ - if (this.hasMomentFormat(dateString)) { - // expect C2822 or ISO format - date = moment(dateString); - } else { - // try to read date string with native Date - date = moment(new Date(dateString)); - } - } else { - date = moment(dateString, dateFormat, locale); - } - - // format date (if it is valid) - return date.isValid() ? date.locale(this.locale).format(this.toFormat) : null; - } - - private hasMomentFormat(dateString: string): boolean { - const date = moment(dateString, true); // strict mode - return date.isValid(); - } -} diff --git a/utils/IconList.ts b/utils/IconList.ts deleted file mode 100644 index c98aa75d..00000000 --- a/utils/IconList.ts +++ /dev/null @@ -1,1185 +0,0 @@ -// credits to phibr0 on discord -export const ICON_LIST = [ - 'activity', - 'airplay', - 'alarm-check', - 'alarm-clock-off', - 'alarm-clock', - 'alarm-minus', - 'alarm-plus', - 'album', - 'alert-circle', - 'alert-octagon', - 'alert-triangle', - 'align-center-horizontal', - 'align-center-vertical', - 'align-center', - 'align-end-horizontal', - 'align-end-vertical', - 'align-horizontal-distribute-center', - 'align-horizontal-distribute-end', - 'align-horizontal-distribute-start', - 'align-horizontal-justify-center', - 'align-horizontal-justify-end', - 'align-horizontal-justify-start', - 'align-horizontal-space-around', - 'align-horizontal-space-between', - 'align-justify', - 'align-left', - 'align-right', - 'align-start-horizontal', - 'align-start-vertical', - 'align-vertical-distribute-center', - 'align-vertical-distribute-end', - 'align-vertical-distribute-start', - 'align-vertical-justify-center', - 'align-vertical-justify-end', - 'align-vertical-justify-start', - 'align-vertical-space-around', - 'align-vertical-space-between', - 'anchor', - 'aperture', - 'archive', - 'arrow-big-down', - 'arrow-big-left', - 'arrow-big-right', - 'arrow-big-up', - 'arrow-down-circle', - 'arrow-down-left', - 'arrow-down-right', - 'arrow-down', - 'arrow-left-circle', - 'arrow-left-right', - 'arrow-left', - 'arrow-right-circle', - 'arrow-right', - 'arrow-up-circle', - 'arrow-up-left', - 'arrow-up-right', - 'arrow-up', - 'asterisk', - 'at-sign', - 'award', - 'axe', - 'banknote', - 'bar-chart-2', - 'bar-chart', - 'baseline', - 'battery-charging', - 'battery-full', - 'battery-low', - 'battery-medium', - 'battery', - 'beaker', - 'bell-minus', - 'bell-off', - 'bell-plus', - 'bell-ring', - 'bell', - 'bike', - 'binary', - 'bitcoin', - 'bluetooth-connected', - 'bluetooth-off', - 'bluetooth-searching', - 'bluetooth', - 'bold', - 'book-open', - 'book', - 'bookmark-minus', - 'bookmark-plus', - 'bookmark', - 'bot', - 'box-select', - 'box', - 'briefcase', - 'brush', - 'bug', - 'building-2', - 'building', - 'bus', - 'calculator', - 'calendar', - 'camera-off', - 'camera', - 'car', - 'carrot', - 'cast', - 'check-circle-2', - 'check-circle', - 'check-square', - 'check', - 'chevron-down', - 'chevron-first', - 'chevron-last', - 'chevron-left', - 'chevron-right', - 'chevron-up', - 'chevrons-down-up', - 'chevrons-down', - 'chevrons-left', - 'chevrons-right', - 'chevrons-up-down', - 'chevrons-up', - 'chrome', - 'circle-slashed', - 'circle', - 'clipboard-check', - 'clipboard-copy', - 'clipboard-list', - 'clipboard-x', - 'clipboard', - 'clock-1', - 'clock-10', - 'clock-11', - 'clock-12', - 'clock-2', - 'clock-3', - 'clock-4', - 'clock-5', - 'clock-6', - 'clock-7', - 'clock-8', - 'clock-9', - 'clock', - 'cloud-drizzle', - 'cloud-fog', - 'cloud-hail', - 'cloud-lightning', - 'cloud-moon', - 'cloud-off', - 'cloud-rain-wind', - 'cloud-rain', - 'cloud-snow', - 'cloud-sun', - 'cloud', - 'cloudy', - 'clover', - 'code-2', - 'code', - 'codepen', - 'codesandbox', - 'coffee', - 'coins', - 'columns', - 'command', - 'compass', - 'contact', - 'contrast', - 'cookie', - 'copy', - 'copyleft', - 'copyright', - 'corner-down-left', - 'corner-down-right', - 'corner-left-down', - 'corner-left-up', - 'corner-right-down', - 'corner-right-up', - 'corner-up-left', - 'corner-up-right', - 'cpu', - 'credit-card', - 'crop', - 'cross', - 'crosshair', - 'crown', - 'currency', - 'database', - 'delete', - 'dice-1', - 'dice-2', - 'dice-3', - 'dice-4', - 'dice-5', - 'dice-6', - 'disc', - 'divide-circle', - 'divide-square', - 'divide', - 'dollar-sign', - 'download-cloud', - 'download', - 'dribbble', - 'droplet', - 'droplets', - 'drumstick', - 'edit-2', - 'edit-3', - 'edit', - 'egg', - 'equal-not', - 'equal', - 'eraser', - 'euro', - 'expand', - 'external-link', - 'eye-off', - 'eye', - 'facebook', - 'fast-forward', - 'feather', - 'figma', - 'file-check-2', - 'file-check', - 'file-code', - 'file-digit', - 'file-input', - 'file-minus-2', - 'file-minus', - 'file-output', - 'file-plus-2', - 'file-plus', - 'file-search', - 'file-text', - 'file-x-2', - 'file-x', - 'file', - 'files', - 'film', - 'filter', - 'flag-off', - 'flag-triangle-left', - 'flag-triangle-right', - 'flag', - 'flame', - 'flashlight-off', - 'flashlight', - 'flask-conical', - 'flask-round', - 'folder-minus', - 'folder-open', - 'folder-plus', - 'folder', - 'form-input', - 'forward', - 'frame', - 'framer', - 'frown', - 'function-square', - 'gamepad-2', - 'gamepad', - 'gauge', - 'gavel', - 'gem', - 'ghost', - 'gift', - 'git-branch-plus', - 'git-branch', - 'git-commit', - 'git-fork', - 'git-merge', - 'git-pull-request', - 'github', - 'gitlab', - 'glasses', - 'globe-2', - 'globe', - 'grab', - 'graduation-cap', - 'grid', - 'grip-horizontal', - 'grip-vertical', - 'hammer', - 'hand-metal', - 'hand', - 'hard-drive', - 'hard-hat', - 'hash', - 'haze', - 'headphones', - 'heart', - 'help-circle', - 'hexagon', - 'highlighter', - 'history', - 'home', - 'image-minus', - 'image-off', - 'image-plus', - 'image', - 'import', - 'inbox', - 'indent', - 'indian-rupee', - 'infinity', - 'info', - 'inspect', - 'instagram', - 'italic', - 'japanese-yen', - 'key', - 'keyboard', - 'landmark', - 'languages', - 'laptop-2', - 'laptop', - 'lasso-select', - 'lasso', - 'layers', - 'layout-dashboard', - 'layout-grid', - 'layout-list', - 'layout-template', - 'layout', - 'library', - 'life-buoy', - 'lightbulb-off', - 'lightbulb', - 'link-2-off', - 'link-2', - 'link', - 'linkedin', - 'list-checks', - 'list-minus', - 'list-ordered', - 'list-plus', - 'list-x', - 'list', - 'loader-2', - 'loader', - 'locate-fixed', - 'locate-off', - 'locate', - 'lock', - 'log-in', - 'log-out', - 'mail', - 'map-pin', - 'map', - 'maximize-2', - 'maximize', - 'megaphone', - 'meh', - 'menu', - 'message-circle', - 'message-square', - 'mic-off', - 'mic', - 'minimize-2', - 'minimize', - 'minus-circle', - 'minus-square', - 'minus', - 'monitor-off', - 'monitor-speaker', - 'monitor', - 'moon', - 'more-horizontal', - 'more-vertical', - 'mountain-snow', - 'mountain', - 'mouse-pointer-2', - 'mouse-pointer-click', - 'mouse-pointer', - 'mouse', - 'move-diagonal-2', - 'move-diagonal', - 'move-horizontal', - 'move-vertical', - 'move', - 'music', - 'navigation-2', - 'navigation', - 'network', - 'octagon', - 'option', - 'outdent', - 'package-check', - 'package-minus', - 'package-plus', - 'package-search', - 'package-x', - 'package', - 'palette', - 'palmtree', - 'paperclip', - 'pause-circle', - 'pause-octagon', - 'pause', - 'pen-tool', - 'pencil', - 'percent', - 'person-standing', - 'phone-call', - 'phone-forwarded', - 'phone-incoming', - 'phone-missed', - 'phone-off', - 'phone-outgoing', - 'phone', - 'pie-chart', - 'piggy-bank', - 'pin', - 'pipette', - 'plane', - 'play-circle', - 'play', - 'plug-zap', - 'plus-circle', - 'plus-square', - 'plus', - 'pocket', - 'podcast', - 'pointer', - 'pound-sterling', - 'power-off', - 'power', - 'printer', - 'qr-code', - 'quote', - 'radio-receiver', - 'radio', - 'redo', - 'refresh-ccw', - 'refresh-cw', - 'regex', - 'repeat-1', - 'repeat', - 'reply-all', - 'reply', - 'rewind', - 'rocket', - 'rocking-chair', - 'rotate-ccw', - 'rotate-cw', - 'rss', - 'ruler', - 'russian-ruble', - 'save', - 'scale', - 'scan-line', - 'scan', - 'scissors', - 'screen-share-off', - 'screen-share', - 'search', - 'send', - 'separator-horizontal', - 'separator-vertical', - 'server-crash', - 'server-off', - 'server', - 'settings-2', - 'settings', - 'share-2', - 'share', - 'sheet', - 'shield-alert', - 'shield-check', - 'shield-close', - 'shield-off', - 'shield', - 'shirt', - 'shopping-bag', - 'shopping-cart', - 'shovel', - 'shrink', - 'shuffle', - 'sidebar-close', - 'sidebar-open', - 'sidebar', - 'sigma', - 'signal-high', - 'signal-low', - 'signal-medium', - 'signal-zero', - 'signal', - 'skip-back', - 'skip-forward', - 'skull', - 'slack', - 'slash', - 'sliders', - 'smartphone-charging', - 'smartphone', - 'smile', - 'snowflake', - 'sort-asc', - 'sort-desc', - 'speaker', - 'sprout', - 'square', - 'star-half', - 'star', - 'stop-circle', - 'stretch-horizontal', - 'stretch-vertical', - 'strikethrough', - 'subscript', - 'sun', - 'sunrise', - 'sunset', - 'superscript', - 'swiss-franc', - 'switch-camera', - 'table', - 'tablet', - 'tag', - 'target', - 'tent', - 'terminal-square', - 'terminal', - 'text-cursor-input', - 'text-cursor', - 'thermometer-snowflake', - 'thermometer-sun', - 'thermometer', - 'thumbs-down', - 'thumbs-up', - 'ticket', - 'timer-off', - 'timer-reset', - 'timer', - 'toggle-left', - 'toggle-right', - 'tornado', - 'trash-2', - 'trash', - 'trello', - 'trending-down', - 'trending-up', - 'triangle', - 'truck', - 'tv-2', - 'tv', - 'twitch', - 'twitter', - 'type', - 'umbrella', - 'underline', - 'undo', - 'unlink-2', - 'unlink', - 'unlock', - 'upload-cloud', - 'upload', - 'user-check', - 'user-minus', - 'user-plus', - 'user-x', - 'user', - 'users', - 'verified', - 'vibrate', - 'video-off', - 'video', - 'view', - 'voicemail', - 'volume-1', - 'volume-2', - 'volume-x', - 'volume', - 'wallet', - 'wand', - 'watch', - 'waves', - 'webcam', - 'wifi-off', - 'wifi', - 'wind', - 'wrap-text', - 'wrench', - 'x-circle', - 'x-octagon', - 'x-square', - 'x', - 'youtube', - 'zap-off', - 'zap', - 'zoom-in', - 'zoom-out', - 'search-large', - 'search', - 'activity', - 'airplay', - 'alarm-check', - 'alarm-clock-off', - 'alarm-clock', - 'alarm-minus', - 'alarm-plus', - 'album', - 'alert-circle', - 'alert-octagon', - 'alert-triangle', - 'align-center-horizontal', - 'align-center-vertical', - 'align-center', - 'align-end-horizontal', - 'align-end-vertical', - 'align-horizontal-distribute-center', - 'align-horizontal-distribute-end', - 'align-horizontal-distribute-start', - 'align-horizontal-justify-center', - 'align-horizontal-justify-end', - 'align-horizontal-justify-start', - 'align-horizontal-space-around', - 'align-horizontal-space-between', - 'align-justify', - 'align-left', - 'align-right', - 'align-start-horizontal', - 'align-start-vertical', - 'align-vertical-distribute-center', - 'align-vertical-distribute-end', - 'align-vertical-distribute-start', - 'align-vertical-justify-center', - 'align-vertical-justify-end', - 'align-vertical-justify-start', - 'align-vertical-space-around', - 'align-vertical-space-between', - 'anchor', - 'aperture', - 'archive', - 'arrow-big-down', - 'arrow-big-left', - 'arrow-big-right', - 'arrow-big-up', - 'arrow-down-circle', - 'arrow-down-left', - 'arrow-down-right', - 'arrow-down', - 'arrow-left-circle', - 'arrow-left-right', - 'arrow-left', - 'arrow-right-circle', - 'arrow-right', - 'arrow-up-circle', - 'arrow-up-left', - 'arrow-up-right', - 'arrow-up', - 'asterisk', - 'at-sign', - 'award', - 'axe', - 'banknote', - 'bar-chart-2', - 'bar-chart', - 'baseline', - 'battery-charging', - 'battery-full', - 'battery-low', - 'battery-medium', - 'battery', - 'beaker', - 'bell-minus', - 'bell-off', - 'bell-plus', - 'bell-ring', - 'bell', - 'bike', - 'binary', - 'bitcoin', - 'bluetooth-connected', - 'bluetooth-off', - 'bluetooth-searching', - 'bluetooth', - 'bold', - 'book-open', - 'book', - 'bookmark-minus', - 'bookmark-plus', - 'bookmark', - 'bot', - 'box-select', - 'box', - 'briefcase', - 'brush', - 'bug', - 'building-2', - 'building', - 'bus', - 'calculator', - 'calendar', - 'camera-off', - 'camera', - 'car', - 'carrot', - 'cast', - 'check-circle-2', - 'check-circle', - 'check-square', - 'check', - 'chevron-down', - 'chevron-first', - 'chevron-last', - 'chevron-left', - 'chevron-right', - 'chevron-up', - 'chevrons-down-up', - 'chevrons-down', - 'chevrons-left', - 'chevrons-right', - 'chevrons-up-down', - 'chevrons-up', - 'chrome', - 'circle-slashed', - 'circle', - 'clipboard-check', - 'clipboard-copy', - 'clipboard-list', - 'clipboard-x', - 'clipboard', - 'clock-1', - 'clock-10', - 'clock-11', - 'clock-12', - 'clock-2', - 'clock-3', - 'clock-4', - 'clock-5', - 'clock-6', - 'clock-7', - 'clock-8', - 'clock-9', - 'lucide-clock', - 'cloud-drizzle', - 'cloud-fog', - 'cloud-hail', - 'cloud-lightning', - 'cloud-moon', - 'cloud-off', - 'cloud-rain-wind', - 'cloud-rain', - 'cloud-snow', - 'cloud-sun', - 'lucide-cloud', - 'cloudy', - 'clover', - 'code-2', - 'code', - 'codepen', - 'codesandbox', - 'coffee', - 'coins', - 'columns', - 'command', - 'compass', - 'contact', - 'contrast', - 'cookie', - 'copy', - 'copyleft', - 'copyright', - 'corner-down-left', - 'corner-down-right', - 'corner-left-down', - 'corner-left-up', - 'corner-right-down', - 'corner-right-up', - 'corner-up-left', - 'corner-up-right', - 'cpu', - 'credit-card', - 'crop', - 'lucide-cross', - 'crosshair', - 'crown', - 'currency', - 'database', - 'delete', - 'dice-1', - 'dice-2', - 'dice-3', - 'dice-4', - 'dice-5', - 'dice-6', - 'disc', - 'divide-circle', - 'divide-square', - 'divide', - 'dollar-sign', - 'download-cloud', - 'download', - 'dribbble', - 'droplet', - 'droplets', - 'drumstick', - 'edit-2', - 'edit-3', - 'edit', - 'egg', - 'equal-not', - 'equal', - 'eraser', - 'euro', - 'expand', - 'external-link', - 'eye-off', - 'eye', - 'facebook', - 'fast-forward', - 'feather', - 'figma', - 'file-check-2', - 'file-check', - 'file-code', - 'file-digit', - 'file-input', - 'file-minus-2', - 'file-minus', - 'file-output', - 'file-plus-2', - 'file-plus', - 'file-search', - 'file-text', - 'file-x-2', - 'file-x', - 'file', - 'files', - 'film', - 'filter', - 'flag-off', - 'flag-triangle-left', - 'flag-triangle-right', - 'flag', - 'flame', - 'flashlight-off', - 'flashlight', - 'flask-conical', - 'flask-round', - 'folder-minus', - 'folder-open', - 'folder-plus', - 'lucide-folder', - 'form-input', - 'forward', - 'frame', - 'framer', - 'frown', - 'function-square', - 'gamepad-2', - 'gamepad', - 'gauge', - 'gavel', - 'gem', - 'ghost', - 'gift', - 'git-branch-plus', - 'git-branch', - 'git-commit', - 'git-fork', - 'git-merge', - 'git-pull-request', - 'github', - 'gitlab', - 'glasses', - 'globe-2', - 'globe', - 'grab', - 'graduation-cap', - 'grid', - 'grip-horizontal', - 'grip-vertical', - 'hammer', - 'hand-metal', - 'hand', - 'hard-drive', - 'hard-hat', - 'hash', - 'haze', - 'headphones', - 'heart', - 'help-circle', - 'hexagon', - 'highlighter', - 'history', - 'home', - 'image-minus', - 'image-off', - 'image-plus', - 'image', - 'import', - 'inbox', - 'indent', - 'indian-rupee', - 'infinity', - 'lucide-info', - 'inspect', - 'instagram', - 'italic', - 'japanese-yen', - 'key', - 'keyboard', - 'landmark', - 'lucide-languages', - 'laptop-2', - 'laptop', - 'lasso-select', - 'lasso', - 'layers', - 'layout-dashboard', - 'layout-grid', - 'layout-list', - 'layout-template', - 'layout', - 'library', - 'life-buoy', - 'lightbulb-off', - 'lightbulb', - 'link-2-off', - 'link-2', - 'lucide-link', - 'linkedin', - 'list-checks', - 'list-minus', - 'list-ordered', - 'list-plus', - 'list-x', - 'list', - 'loader-2', - 'loader', - 'locate-fixed', - 'locate-off', - 'locate', - 'lock', - 'log-in', - 'log-out', - 'mail', - 'map-pin', - 'map', - 'maximize-2', - 'maximize', - 'megaphone', - 'meh', - 'menu', - 'message-circle', - 'message-square', - 'mic-off', - 'mic', - 'minimize-2', - 'minimize', - 'minus-circle', - 'minus-square', - 'minus', - 'monitor-off', - 'monitor-speaker', - 'monitor', - 'moon', - 'more-horizontal', - 'more-vertical', - 'mountain-snow', - 'mountain', - 'mouse-pointer-2', - 'mouse-pointer-click', - 'mouse-pointer', - 'mouse', - 'move-diagonal-2', - 'move-diagonal', - 'move-horizontal', - 'move-vertical', - 'move', - 'music', - 'navigation-2', - 'navigation', - 'network', - 'octagon', - 'option', - 'outdent', - 'package-check', - 'package-minus', - 'package-plus', - 'package-search', - 'package-x', - 'package', - 'palette', - 'palmtree', - 'paperclip', - 'pause-circle', - 'pause-octagon', - 'pause', - 'pen-tool', - 'lucide-pencil', - 'percent', - 'person-standing', - 'phone-call', - 'phone-forwarded', - 'phone-incoming', - 'phone-missed', - 'phone-off', - 'phone-outgoing', - 'phone', - 'pie-chart', - 'piggy-bank', - 'lucide-pin', - 'pipette', - 'plane', - 'play-circle', - 'play', - 'plug-zap', - 'plus-circle', - 'plus-square', - 'plus', - 'pocket', - 'podcast', - 'pointer', - 'pound-sterling', - 'power-off', - 'power', - 'printer', - 'qr-code', - 'quote', - 'radio-receiver', - 'radio', - 'redo', - 'refresh-ccw', - 'refresh-cw', - 'regex', - 'repeat-1', - 'repeat', - 'reply-all', - 'reply', - 'rewind', - 'rocket', - 'rocking-chair', - 'rotate-ccw', - 'rotate-cw', - 'rss', - 'ruler', - 'russian-ruble', - 'save', - 'scale', - 'scan-line', - 'scan', - 'scissors', - 'screen-share-off', - 'screen-share', - 'lucide-search', - 'send', - 'separator-horizontal', - 'separator-vertical', - 'server-crash', - 'server-off', - 'server', - 'settings-2', - 'settings', - 'share-2', - 'share', - 'sheet', - 'shield-alert', - 'shield-check', - 'shield-close', - 'shield-off', - 'shield', - 'shirt', - 'shopping-bag', - 'shopping-cart', - 'shovel', - 'shrink', - 'shuffle', - 'sidebar-close', - 'sidebar-open', - 'sidebar', - 'sigma', - 'signal-high', - 'signal-low', - 'signal-medium', - 'signal-zero', - 'signal', - 'skip-back', - 'skip-forward', - 'skull', - 'slack', - 'slash', - 'sliders', - 'smartphone-charging', - 'smartphone', - 'smile', - 'snowflake', - 'sort-asc', - 'sort-desc', - 'speaker', - 'sprout', - 'square', - 'star-half', - 'lucide-star', - 'stop-circle', - 'stretch-horizontal', - 'stretch-vertical', - 'strikethrough', - 'subscript', - 'sun', - 'sunrise', - 'sunset', - 'superscript', - 'swiss-franc', - 'switch-camera', - 'table', - 'tablet', - 'tag', - 'target', - 'tent', - 'terminal-square', - 'terminal', - 'text-cursor-input', - 'text-cursor', - 'thermometer-snowflake', - 'thermometer-sun', - 'thermometer', - 'thumbs-down', - 'thumbs-up', - 'ticket', - 'timer-off', - 'timer-reset', - 'timer', - 'toggle-left', - 'toggle-right', - 'tornado', - 'trash-2', - 'lucide-trash', - 'trello', - 'trending-down', - 'trending-up', - 'triangle', - 'truck', - 'tv-2', - 'tv', - 'twitch', - 'twitter', - 'type', - 'umbrella', - 'underline', - 'undo', - 'unlink-2', - 'unlink', - 'unlock', - 'upload-cloud', - 'upload', - 'user-check', - 'user-minus', - 'user-plus', - 'user-x', - 'user', - 'users', - 'verified', - 'vibrate', - 'video-off', - 'video', - 'view', - 'voicemail', - 'volume-1', - 'volume-2', - 'volume-x', - 'volume', - 'wallet', - 'wand', - 'watch', - 'waves', - 'webcam', - 'wifi-off', - 'wifi', - 'wind', - 'wrap-text', - 'wrench', - 'x-circle', - 'x-octagon', - 'x-square', - 'x', - 'youtube', - 'zap-off', - 'zap', - 'zoom-in', - 'zoom-out', - 'search-large', - 'lucide-search', -]; diff --git a/utils/IllegalFilenameCharactersList.ts b/utils/IllegalFilenameCharactersList.ts deleted file mode 100644 index 23332276..00000000 --- a/utils/IllegalFilenameCharactersList.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Illegal characters in the form `[illegal_character, replacement][]` -export const ILLEGAL_FILENAME_CHARACTERS = [ - ['/', '-'], - ['\\', '-'], - ['<', ''], - ['>', ''], - [':', ' - '], - ['"', "'"], - ['|', ' - '], - ['?', ''], - ['*', ''], - ['^', ''], - ['#', ''], -]; diff --git a/utils/MediaType.ts b/utils/MediaType.ts deleted file mode 100644 index a9dfb548..00000000 --- a/utils/MediaType.ts +++ /dev/null @@ -1,13 +0,0 @@ -export enum MediaType { - Artist = 'artist', - BoardGame = 'boardgame', - Book = 'book', - ComicManga = 'comicManga', - Game = 'game', - Movie = 'movie', - MusicRelease = 'musicRelease', - Season = 'season', - Series = 'series', - Song = 'song', - Wiki = 'wiki', -} diff --git a/utils/MediaTypeManager.ts b/utils/MediaTypeManager.ts deleted file mode 100644 index 3b51f8b3..00000000 --- a/utils/MediaTypeManager.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { App, TFile } from 'obsidian'; -import { TFolder } from 'obsidian'; -import { ArtistModel } from '../models/ArtistModel'; -import { BoardGameModel } from '../models/BoardGameModel'; -import { BookModel } from '../models/BookModel'; -import { ComicMangaModel } from '../models/ComicMangaModel'; -import { GameModel } from '../models/GameModel'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MovieModel } from '../models/MovieModel'; -import { MusicReleaseModel } from '../models/MusicReleaseModel'; -import { SeasonModel } from '../models/SeasonModel'; -import { SeriesModel } from '../models/SeriesModel'; -import { SongModel } from '../models/SongModel'; -import { WikiModel } from '../models/WikiModel'; -import type { MediaDbPluginSettings } from '../settings/Settings'; -import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; -import { MediaType } from './MediaType'; -import { replaceTags } from './Utils'; - -// All media types in alphabetical order -export const MEDIA_TYPES: MediaType[] = [ - MediaType.Artist, - MediaType.BoardGame, - MediaType.Book, - MediaType.ComicManga, - MediaType.Game, - MediaType.Movie, - MediaType.MusicRelease, - MediaType.Series, - MediaType.Season, - MediaType.Song, - MediaType.Wiki, -]; - -export class MediaTypeManager { - mediaFileNameTemplateMap: Map; - mediaTemplateMap: Map; - mediaFolderMap: Map; - - constructor() { - this.mediaFileNameTemplateMap = new Map(); - this.mediaTemplateMap = new Map(); - this.mediaFolderMap = new Map(); - } - - updateTemplates(settings: MediaDbPluginSettings): void { - this.mediaFileNameTemplateMap = new Map(); - this.mediaFileNameTemplateMap.set(MediaType.Artist, settings.artistFileNameTemplate); - 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); - this.mediaFileNameTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseFileNameTemplate); - this.mediaFileNameTemplateMap.set(MediaType.BoardGame, settings.boardgameFileNameTemplate); - this.mediaFileNameTemplateMap.set(MediaType.Book, settings.bookFileNameTemplate); - this.mediaFileNameTemplateMap.set(MediaType.Song, settings.songFileNameTemplate); - - this.mediaTemplateMap = new Map(); - this.mediaTemplateMap.set(MediaType.Artist, settings.artistTemplate); - 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); - this.mediaTemplateMap.set(MediaType.MusicRelease, settings.musicReleaseTemplate); - this.mediaTemplateMap.set(MediaType.BoardGame, settings.boardgameTemplate); - this.mediaTemplateMap.set(MediaType.Book, settings.bookTemplate); - this.mediaTemplateMap.set(MediaType.Song, settings.songTemplate); - } - - updateFolders(settings: MediaDbPluginSettings): void { - this.mediaFolderMap = new Map(); - this.mediaFolderMap.set(MediaType.Artist, settings.artistFolder); - 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); - this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); - this.mediaFolderMap.set(MediaType.BoardGame, settings.boardgameFolder); - this.mediaFolderMap.set(MediaType.Book, settings.bookFolder); - this.mediaFolderMap.set(MediaType.Song, settings.songFolder); - } - - getFileName(mediaTypeModel: MediaTypeModel): string { - // Ignore undefined tags since some search APIs do not return all properties in the model and produce clean file names even if errors occur - const fileName = replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType())!, mediaTypeModel, true); - return this.cleanFileName(fileName); - } - - cleanFileName(fileName: string): string { - const cleanedFileName = ILLEGAL_FILENAME_CHARACTERS.reduce((str, char) => str.replaceAll(char[0], char[1]), fileName); - // Remove all duplicate whitespace in the file name - return cleanedFileName.replaceAll(/ +/g, ' '); - } - - async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise { - const templateFilePath = this.mediaTemplateMap.get(mediaTypeModel.getMediaType()); - - if (!templateFilePath) { - return ''; - } - - let templateFile = app.vault.getAbstractFileByPath(templateFilePath) ?? undefined; - - // WARNING: This was previously selected by filename, but that could lead to collisions and unwanted effects. - // This now falls back to the previous method if no file is found - if (!templateFile || templateFile instanceof TFolder) { - templateFile = app.vault - .getFiles() - .filter((f: TFile) => f.name === templateFilePath) - .first(); - - if (!templateFile) { - return ''; - } - } - - const template = await app.vault.cachedRead(templateFile as TFile); - // console.log(template); - return replaceTags(template, mediaTypeModel); - } - - async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise { - let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); - - folderPath ??= `/`; - // console.log(folderPath); - - if (!(await app.vault.adapter.exists(folderPath))) { - await app.vault.createFolder(folderPath); - } - const folder = app.vault.getAbstractFileByPath(folderPath); - - if (!(folder instanceof TFolder)) { - throw Error(`Expected ${folder?.path} to be instance of TFolder`); - } - - return folder; - } - - /** - * Takes an object and a MediaType and turns the object into an instance of a MediaTypeModel corresponding to the MediaType passed in. - * - * @param obj - * @param mediaType - */ - createMediaTypeModelFromMediaType(obj: object, mediaType: MediaType): MediaTypeModel { - if (mediaType === MediaType.Movie) { - 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) { - return new GameModel(obj); - } else if (mediaType === MediaType.Wiki) { - return new WikiModel(obj); - } else if (mediaType === MediaType.MusicRelease) { - return new MusicReleaseModel(obj); - } else if (mediaType === MediaType.BoardGame) { - return new BoardGameModel(obj); - } else if (mediaType === MediaType.Book) { - return new BookModel(obj); - } else if (mediaType === MediaType.Artist) { - return new ArtistModel(obj); - } else if (mediaType === MediaType.Song) { - return new SongModel(obj); - } - - throw new Error(`Unknown media type: ${mediaType}`); - } -} diff --git a/utils/ModalHelper.ts b/utils/ModalHelper.ts deleted file mode 100644 index 7efba29f..00000000 --- a/utils/ModalHelper.ts +++ /dev/null @@ -1,537 +0,0 @@ -import { Notice } from 'obsidian'; -import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal'; -import type MediaDbPlugin from '../main'; -import { MediaDbAdvancedSearchModal } from '../modals/MediaDbAdvancedSearchModal'; -import { MediaDbIdSearchModal } from '../modals/MediaDbIdSearchModal'; -import { MediaDbSearchModal } from '../modals/MediaDbSearchModal'; -import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from './MediaType'; - -export enum ModalResultCode { - SUCCESS = 'SUCCESS', - SKIP = 'SKIP', - CLOSE = 'CLOSE', - ERROR = 'ERROR', -} - -type ModalResult = - | { - code: ModalResultCode.CLOSE; - } - | { - code: ModalResultCode.ERROR; - error: Error; - } - | { - code: ModalResultCode.SUCCESS; - data: T; - }; - -type SkippableModalResult = - | ModalResult - | { - code: ModalResultCode.SKIP; - }; - -/** - * Object containing the data {@link ModalHelper.createSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type AdvancedSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createIdSearchModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link IdSearchModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type IdSearchModalResult = ModalResult; - -/** - * Object containing the data {@link ModalHelper.createSelectModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link SelectModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type SelectModalResult = SkippableModalResult; - -/** - * Object containing the data {@link ModalHelper.createPreviewModal} returns. - * On {@link ModalResultCode.SUCCESS} this contains {@link PreviewModalData}. - * On {@link ModalResultCode.ERROR} this contains a reference to that error. - */ -export type PreviewModalResult = ModalResult; - -/** - * The data the search modal returns. - * - query: the query string - * - types: the selected APIs - */ -export interface SearchModalData { - query: string; - types: MediaType[]; -} - -/** - * The data the advanced search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface AdvancedSearchModalData { - query: string; - apis: string[]; -} - -/** - * The data the id search modal returns. - * - query: the query string - * - apis: the selected APIs - */ -export interface IdSearchModalData { - query: string; - api: string; -} - -/** - * The data the select modal returns. - * - selected: the selected items - */ -export interface SelectModalData { - selected: MediaTypeModel[]; -} - -/** - * The data the preview modal returns. - * - confirmed: whether the selected element has been confirmed - */ -export interface PreviewModalData { - confirmed: boolean; -} - -/** - * Options for the search modal. - * - modalTitle: the title of the modal - * - preselectedTypes: a list of preselected Types - * - prefilledSearchString: prefilled query - */ -export interface SearchModalOptions { - modalTitle?: string; - preselectedTypes?: MediaType[]; - prefilledSearchString?: string; -} - -/** - * Options for the advanced search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface AdvancedSearchModalOptions { - modalTitle?: string; - preselectedAPIs?: string[]; - prefilledSearchString?: string; -} - -/** - * Options for the id search modal. - * - modalTitle: the title of the modal - * - preselectedAPIs: a list of preselected APIs - * - prefilledSearchString: prefilled query - */ -export interface IdSearchModalOptions { - modalTitle?: string; - preselectedAPI?: string; - prefilledSearchString?: string; -} - -/** - * Options for the select modal. - * - modalTitle: the title of the modal - * - elements: the elements the user can select from - * - multiSelect: whether to allow multiselect - * - skipButton: whether to add a skip button to the modal - */ -export interface SelectModalOptions { - elements?: MediaTypeModel[]; - multiSelect?: boolean; - modalTitle?: string; - skipButton?: boolean; - description?: string; // Add this - submitButtonText?: string; // Add this too -} -/** - * Options for the preview modal. - * - modalTitle: the title of the modal - * - elements: the elements to preview - */ -export interface PreviewModalOptions { - modalTitle?: string; - elements?: MediaTypeModel[]; -} - -export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = { - modalTitle: 'Media DB Search', - preselectedTypes: [], - prefilledSearchString: '', -}; - -export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = { - modalTitle: 'Media DB Advanced Search', - preselectedAPIs: [], - prefilledSearchString: '', -}; - -export const ID_SEARCH_MODAL_DEFAULT_OPTIONS: IdSearchModalOptions = { - modalTitle: 'Media DB Id Search', - preselectedAPI: undefined, - prefilledSearchString: '', -}; - -export const SELECT_MODAL_OPTIONS_DEFAULT: SelectModalOptions = { - modalTitle: 'Media DB Search Results', - elements: [], - multiSelect: true, - skipButton: false, -}; - -export const PREVIEW_MODAL_DEFAULT_OPTIONS: PreviewModalOptions = { - modalTitle: 'Media DB Preview', - elements: [], -}; - -export const SELECTMODALOPTIONSDEFAULT: SelectModalOptions = { - elements: [], - multiSelect: true, - modalTitle: '', - skipButton: false, - description: 'Select one or multiple search results.', - submitButtonText: 'Ok', -}; - -/** - * A class providing multiple usefull functions for dealing with the plugins modals. - */ -export class ModalHelper { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - /** - * Creates an {@link MediaDbSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult; searchModal: MediaDbSearchModal }> { - const modal = new MediaDbSearchModal(this.plugin, searchModalOptions); - const res: SearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { searchModalResult: res, searchModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSearchModal( - searchModalOptions: SearchModalOptions, - submitCallback: (searchModalData: SearchModalData) => Promise, - ): Promise { - const { searchModalResult, searchModal } = await this.createSearchModal(searchModalOptions); - console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`); - - if (searchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(searchModalResult.error); - new Notice(searchModalResult.error.toString()); - searchModal.close(); - return undefined; - } - - if (searchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(searchModalResult.data); - searchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - searchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - ): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult; advancedSearchModal: MediaDbAdvancedSearchModal }> { - const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions); - const res: AdvancedSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { advancedSearchModalResult: res, advancedSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbAdvancedSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param advancedSearchModalOptions the options for the modal, see {@link ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openAdvancedSearchModal( - advancedSearchModalOptions: AdvancedSearchModalOptions, - submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise, - ): Promise { - const { advancedSearchModalResult, advancedSearchModal } = await this.createAdvancedSearchModal(advancedSearchModalOptions); - console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`); - - if (advancedSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(advancedSearchModalResult.error); - new Notice(advancedSearchModalResult.error.toString()); - advancedSearchModal.close(); - return undefined; - } - - if (advancedSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(advancedSearchModalResult.data); - advancedSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - advancedSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbIdSearchModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @returns the user input or nothing and a reference to the modal. - */ - async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult; idSearchModal: MediaDbIdSearchModal }> { - const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions); - const res: IdSearchModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { idSearchModalResult: res, idSearchModal: modal }; - } - - /** - * Opens an {@link MediaDbIdSearchModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param idSearchModalOptions the options for the modal, see {@link ID_SEARCH_MODAL_DEFAULT_OPTIONS} - * @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openIdSearchModal( - idSearchModalOptions: IdSearchModalOptions, - submitCallback: (idSearchModalData: IdSearchModalData) => Promise, - ): Promise { - const { idSearchModalResult, idSearchModal } = await this.createIdSearchModal(idSearchModalOptions); - console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`); - - if (idSearchModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(idSearchModalResult.error); - new Notice(idSearchModalResult.error.toString()); - idSearchModal.close(); - return undefined; - } - - if (idSearchModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - try { - const callbackRes = await submitCallback(idSearchModalResult.data); - idSearchModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - idSearchModal.close(); - return undefined; - } - } - - /** - * Creates an {@link MediaDbSearchResultModal}, then sets callbacks and awaits them, - * returning either the user input once submitted or nothing once closed. - * The modal needs ot be manually closed by calling `close()` on the modal reference. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @returns the user input or nothing and a reference to the modal. - */ - async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult; selectModal: MediaDbSearchResultModal }> { - const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions); - const res: SelectModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setSkipCallback(() => resolve({ code: ModalResultCode.SKIP })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { selectModalResult: res, selectModal: modal }; - } - - /** - * Opens an {@link MediaDbSearchResultModal} and awaits its result, - * then executes the `submitCallback` returning the callbacks result and closing the modal. - * - * @param selectModalOptions the options for the modal, see {@link SELECT_MODAL_OPTIONS_DEFAULT} - * @param submitCallback the callback that gets executed after the modal has been submitted, but before it has been closed - * @returns the user input or nothing and a reference to the modal. - */ - async openSelectModal( - selectModalOptions: SelectModalOptions, - submitCallback: (selectModalData: SelectModalData) => Promise, - ): Promise { - const { selectModalResult, selectModal } = await this.createSelectModal(selectModalOptions); - console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`); - - if (selectModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(selectModalResult.error); - new Notice(selectModalResult.error.toString()); - selectModal.close(); - return undefined; - } - - if (selectModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return undefined; - } - - if (selectModalResult.code === ModalResultCode.SKIP) { - // selection was skipped - return undefined; - } - - try { - const callbackRes: MediaTypeModel[] = await submitCallback(selectModalResult.data); - selectModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - selectModal.close(); - return; - } - } - - async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult; previewModal: MediaDbPreviewModal }> { - //todo: handle attachFile for existing files - const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions); - const res: PreviewModalResult = await new Promise(resolve => { - modal.setSubmitCb(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); - modal.setCloseCb(err => { - if (err) { - resolve({ code: ModalResultCode.ERROR, error: err }); - } - resolve({ code: ModalResultCode.CLOSE }); - }); - - modal.open(); - }); - return { previewModalResult: res, previewModal: modal }; - } - - async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise): Promise { - const { previewModalResult, previewModal } = await this.createPreviewModal(previewModalOptions); - console.debug(`MDB | previewModal closed with code ${previewModalResult.code}`); - - if (previewModalResult.code === ModalResultCode.ERROR) { - // there was an error in the modal itself - console.warn(previewModalResult.error); - new Notice(previewModalResult.error.toString()); - previewModal.close(); - return false; - } - - if (previewModalResult.code === ModalResultCode.CLOSE) { - // modal is already being closed - return false; - } - - try { - const callbackRes: boolean = await submitCallback(previewModalResult.data); - previewModal.close(); - return callbackRes; - } catch (e) { - console.warn(e); - new Notice(`${e}`); - previewModal.close(); - return true; - } - } -} diff --git a/utils/Utils.ts b/utils/Utils.ts deleted file mode 100644 index b4a16b99..00000000 --- a/utils/Utils.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { iso6392 } from 'iso-639-2'; -import type { TFile, TFolder, App } from 'obsidian'; -import { requestUrl } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MediaType } from './MediaType'; - -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.5.2'; -export const debug: boolean = true; - -export function wrapAround(value: number, size: number): number { - if (size <= 0) { - throw Error('size may not be zero or negative'); - } - return mod(value, size); -} - -export function containsOnlyLettersAndUnderscores(str: string): boolean { - return /^[\p{Letter}\p{M}_]+$/u.test(str); -} - -export function replaceIllegalFileNameCharactersInString(string: string): string { - return string.replace(/[\\/:"*?<>|]/g, '-'); -} - -export function replaceTags(template: string, mediaTypeModel: MediaTypeModel, ignoreUndefined: boolean = false): string { - return template.replace(new RegExp('{{.*?}}', 'g'), (match: string) => replaceTag(match, mediaTypeModel, ignoreUndefined)); -} - -function replaceTag(match: string, mediaTypeModel: MediaTypeModel, ignoreUndefined: boolean): string { - let tag = match; - tag = tag.substring(2); - tag = tag.substring(0, tag.length - 2); - tag = tag.trim(); - - const parts = tag.split(':'); - if (parts.length === 1) { - const path = parts[0].split('.'); - - const obj = traverseMetaData(path, mediaTypeModel); - - if (obj === undefined) { - return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; - } - // year: 0 means "unknown" — return empty string so filename templates stay clean (e.g. "Title ()") - if (path[path.length - 1] === 'year' && obj === 0) { - return ''; - } - - // eslint-disable-next-line @typescript-eslint/no-base-to-string - return obj?.toString() ?? 'null'; - } else if (parts.length === 2) { - const operator = parts[0]; - - const path = parts[1].split('.'); - - const obj = traverseMetaData(path, mediaTypeModel); - - if (obj === undefined) { - return ignoreUndefined ? '' : '{{ INVALID TEMPLATE TAG - object undefined }}'; - } - - if (operator === 'LIST') { - if (!Array.isArray(obj)) { - return '{{ INVALID TEMPLATE TAG - operator LIST is only applicable on an array }}'; - } - - return obj.map((e: unknown) => `- ${e}`).join('\n'); - } else if (operator === 'ENUM') { - if (!Array.isArray(obj)) { - return '{{ INVALID TEMPLATE TAG - operator ENUM is only applicable on an array }}'; - } - return obj.join(', '); - } else if (operator === 'FIRST') { - if (!Array.isArray(obj)) { - return '{{ INVALID TEMPLATE TAG - operator FIRST is only applicable on an array }}'; - } - - const first = obj[0] as unknown; - return first?.toString() ?? 'null'; - } else if (operator === 'LAST') { - if (!Array.isArray(obj)) { - return '{{ INVALID TEMPLATE TAG - operator LAST is only applicable on an array }}'; - } - - const last = obj[obj.length - 1] as unknown; - return last?.toString() ?? 'null'; - } - - return `{{ INVALID TEMPLATE TAG - unknown operator ${operator} }}`; - } - - return '{{ INVALID TEMPLATE TAG }}'; -} - -function traverseMetaData(path: string[], mediaTypeModel: MediaTypeModel): unknown { - let o: unknown = mediaTypeModel; - - for (const part of path) { - if (o !== undefined) { - o = (o as Record)[part]; - } - } - - return o; -} - -export function markdownTable(content: string[][]): string { - const rows = content.length; - if (rows === 0) { - return ''; - } - - const columns = content[0].length; - if (columns === 0) { - return ''; - } - for (const row of content) { - if (row.length !== columns) { - return ''; - } - } - - const longestStringInColumns: number[] = []; - - for (let i = 0; i < columns; i++) { - let longestStringInColumn = 0; - for (const row of content) { - if (row[i].length > longestStringInColumn) { - longestStringInColumn = row[i].length; - } - } - - longestStringInColumns.push(longestStringInColumn); - } - - let table = ''; - - for (let i = 0; i < rows; i++) { - table += '|'; - for (let j = 0; j < columns; j++) { - let element = content[i][j]; - element += ' '.repeat(longestStringInColumns[j] - element.length); - table += ' ' + element + ' |'; - } - table += '\n'; - if (i === 0) { - table += '|'; - for (let j = 0; j < columns; j++) { - table += ' ' + '-'.repeat(longestStringInColumns[j]) + ' |'; - } - table += '\n'; - } - } - - return table; -} - -export function fragWithHTML(html: string): DocumentFragment { - return createFragment(frag => (frag.createDiv().innerHTML = html)); -} - -export function dateToString(date: Date): string { - return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; -} - -export function timeToString(time: Date): string { - return `${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`; -} - -export function dateTimeToString(dateTime: Date): string { - return `${dateToString(dateTime)} ${timeToString(dateTime)}`; -} - -// js can't even implement modulo correctly... -export function mod(n: number, m: number): number { - return ((n % m) + m) % m; -} - -export function capitalizeFirstLetter(string: string): string { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -export class PropertyMappingValidationError extends Error { - constructor(message: string) { - super(message); - } -} - -export class PropertyMappingNameConflictError extends Error { - constructor(message: string) { - super(message); - } -} - -/** - * - attachTemplate: whether to attach the template (DEFAULT: false) - * - attachFie: a file to attach (DEFAULT: undefined) - * - openNote: whether to open the note after creation (DEFAULT: false) - * - folder: folder to put the note in - */ -export interface CreateNoteOptions { - attachTemplate?: boolean; - attachFile?: TFile; - openNote?: boolean; - folder?: TFolder; - overwrite?: boolean; -} - -/** Runtime in whole minutes (TMDB/OMDb/MAL). 0 when unknown. Parses legacy string frontmatter (e.g. "136 min", "2 hr 5 min"). */ -export function coerceMovieDurationMinutes(value: unknown): number { - if (value === undefined || value === null) { - return 0; - } - if (typeof value === 'number') { - const n = Math.trunc(value); - return Number.isFinite(n) && n >= 0 ? n : 0; - } - if (typeof value === 'string') { - const t = value.trim(); - if (t === '' || t.toLowerCase() === 'unknown' || t.toUpperCase() === 'N/A' || t === 'TBA') { - return 0; - } - let total = 0; - const hours = t.match(/(\d+)\s*(?:hours?|hrs?)\b/i) ?? t.match(/(\d+)\s*h\b/i); - const mins = t.match(/(\d+)\s*(?:minutes?|mins?)\b/i) ?? t.match(/(\d+)\s*min\b/i); - if (hours) { - total += parseInt(hours[1], 10) * 60; - } - if (mins) { - total += parseInt(mins[1], 10); - } - if (total > 0) { - return total; - } - const n = parseInt(t, 10); - return Number.isFinite(n) && n >= 0 ? n : 0; - } - return 0; -} - -/** Normalizes release year for metadata: integer, 0 when unknown or non-numeric. */ -export function coerceYear(value: unknown): number { - if (value === undefined || value === null) return 0; - if (typeof value === 'number') { - const n = Math.trunc(value); - return Number.isFinite(n) ? n : 0; - } - if (typeof value === 'string') { - const t = value.trim(); - if (t === '' || t.toLowerCase() === 'unknown' || t === 'TBA' || t.toUpperCase() === 'N/A') { - return 0; - } - const n = parseInt(t, 10); - return Number.isFinite(n) ? n : 0; - } - return 0; -} - -export function migrateObject(object: T, oldData: Record, defaultData: T): void { - for (const key in object) { - const has = Object.hasOwn(oldData, key) && oldData[key] !== undefined && oldData[key] !== null; - if (!has) { - object[key] = defaultData[key]; - continue; - } - const raw = oldData[key]; - if (key === 'year') { - (object as Record)[key] = coerceYear(raw); - continue; - } - object[key] = raw as T[typeof key]; - } -} - -export function unCamelCase(str: string): string { - return ( - str - // insert a space between lower & upper - .replace(/([a-z])([A-Z])/g, '$1 $2') - // space before last upper in a sequence followed by lower - .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') - // uppercase the first character - .replace(/^./, function (str) { - return str.toUpperCase(); - }) - ); -} - -/** User-facing label for a media type (e.g. MusicRelease → Album). */ -export function mediaTypeDisplayName(mediaType: MediaType): string { - if (mediaType === MediaType.MusicRelease) { - return 'Album'; - } - return unCamelCase(mediaType); -} - -/* eslint-disable */ - -export function hasTemplaterPlugin(app: App): boolean { - const templater = (app as any).plugins.plugins['templater-obsidian']; - - return !!templater; -} - -// Copied from https://github.com/anpigon/obsidian-book-search-plugin -// Licensed under the MIT license. Copyright (c) 2020 Jake Runzer -export async function useTemplaterPluginInFile(app: App, file: TFile): Promise { - const templater = (app as any).plugins.plugins['templater-obsidian']; - if (templater && !templater?.settings.trigger_on_file_creation) { - await templater.templater.overwrite_file_commands(file); - } -} - -/* eslint-enable */ - -/** Whole USD amounts as used by TMDB (empty when unknown or non-positive). */ -export function formatUsdWholeDollars(amount: number): string { - if (!Number.isFinite(amount) || amount <= 0) { - return ''; - } - return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount); -} - -export type ModelToData = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - [K in keyof T as T[K] extends Function ? never : K]?: T[K] | null; -}; - -// Checks if a given URL points to an existing image (status 200), or returns false for 404/other errors. - -export async function imageUrlExists(url: string): Promise { - try { - // @ts-ignore - const response = await requestUrl({ - url, - method: 'HEAD', - throw: false, - }); - return response.status === 200; - } catch { - return false; - } -} - -export function isTruthy(value: T): value is Exclude { - return Boolean(value); -} - -/** - * Wraps Obsidians `requestUrl` in a fetch like API. - */ -export async function obsidianFetch(input: Request): Promise { - const obs_headers: Record = {}; - input.headers.forEach((header, value) => { - obs_headers[header] = value; - }); - - const res = await requestUrl({ - url: input.url, - method: input.method, - headers: obs_headers, - throw: false, // Do not throw on error, handle it manually - }); - - const responseHeaders: Headers = new Headers(); - for (const [key, value] of Object.entries(res.headers)) { - responseHeaders.append(key, value); - } - - return { - ok: res.status >= 200 && res.status < 300, - status: res.status, - headers: responseHeaders, - // eslint-disable-next-line - json: async () => res.json, - text: async () => res.text, - } as Response; -} - -export function getLanguageName(code: string): string | null { - const language = iso6392.find(lang => lang.iso6392B === code || lang.iso6392T === code); - - return language?.name ?? null; -} diff --git a/utils/normalizeTitleForAlias.ts b/utils/normalizeTitleForAlias.ts deleted file mode 100644 index fd229be0..00000000 --- a/utils/normalizeTitleForAlias.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * ASCII-style form of a title for use as an Obsidian `aliases` entry (e.g. Likbør → Likbor). - * Returns null when the title should not get an extra alias (unchanged after normalization). - * - * NFKD + stripping marks handles most Latin letters; pairs below cover letters that do not - * decompose usefully (ligatures, stroke letters, eth/thorn, eng, Turkish dotless i, …). - */ -const ASCII_ALIAS_FOLDS: readonly [string, string][] = [ - ['æ', 'ae'], - ['Æ', 'Ae'], - ['œ', 'oe'], - ['Œ', 'Oe'], - ['ø', 'o'], - ['Ø', 'O'], - ['ß', 'ss'], - ['ẞ', 'SS'], - ['ð', 'd'], - ['Ð', 'D'], - ['þ', 'th'], - ['Þ', 'Th'], - ['đ', 'd'], - ['Đ', 'D'], - ['ı', 'i'], - ['ħ', 'h'], - ['Ħ', 'H'], - ['ŋ', 'ng'], - ['Ŋ', 'Ng'], - ['Ł', 'L'], - ['ł', 'l'], - ['Ŀ', 'L'], - ['ŀ', 'l'], - ['ĸ', 'k'], - ['ʼn', "'n"], - ['ƿ', 'w'], -]; - -export function normalizeTitleForAsciiAlias(title: string): string | null { - const trimmed = title.trim(); - if (!trimmed) { - return null; - } - - let s = trimmed.normalize('NFKD').replace(/\p{M}/gu, ''); - for (const [from, to] of ASCII_ALIAS_FOLDS) { - s = s.replaceAll(from, to); - } - - if (s === trimmed) { - return null; - } - return s; -} diff --git a/utils/noteTypeSettings.ts b/utils/noteTypeSettings.ts deleted file mode 100644 index 31b51d70..00000000 --- a/utils/noteTypeSettings.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { MediaDbPluginSettings } from '../settings/Settings'; -import { MEDIA_TYPES } from './MediaTypeManager'; -import { MediaType } from './MediaType'; - -const MEDIA_TYPE_TO_NOTE_TYPE_KEY: Record = { - [MediaType.Artist]: 'artistNoteType', - [MediaType.BoardGame]: 'boardgameNoteType', - [MediaType.Book]: 'bookNoteType', - [MediaType.ComicManga]: 'mangaNoteType', - [MediaType.Game]: 'gameNoteType', - [MediaType.Movie]: 'movieNoteType', - [MediaType.MusicRelease]: 'musicReleaseNoteType', - [MediaType.Season]: 'seasonNoteType', - [MediaType.Series]: 'seriesNoteType', - [MediaType.Song]: 'songNoteType', - [MediaType.Wiki]: 'wikiNoteType', -}; - -/** - * Value written to frontmatter `type` for this media kind. Falls back to the internal - * {@link MediaType} string when the setting is empty. - */ -export function noteTypeValueForMedia(settings: MediaDbPluginSettings, mediaType: MediaType): string { - const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; - const raw = settings[key]; - const s = typeof raw === 'string' ? raw.trim() : ''; - return s !== '' ? s : mediaType; -} - -export function setNoteTypeForMedia(settings: MediaDbPluginSettings, mediaType: MediaType, value: string): void { - const key = MEDIA_TYPE_TO_NOTE_TYPE_KEY[mediaType]; - (settings as unknown as Record)[key as string] = value; -} - -/** - * Maps a frontmatter `type` string (legacy enum id or configured custom string) to {@link MediaType}. - */ -export function resolveMetadataTypeToMediaType( - settings: MediaDbPluginSettings, - noteType: unknown, -): MediaType | undefined { - if (noteType === undefined || noteType === null) { - return undefined; - } - let s = String(noteType).trim(); - if (s === '') { - return undefined; - } - if (s === 'manga') { - s = MediaType.ComicManga; - } - for (const mt of MEDIA_TYPES) { - if (mt === s) { - return mt; - } - } - for (const mt of MEDIA_TYPES) { - if (noteTypeValueForMedia(settings, mt) === s) { - return mt; - } - } - return undefined; -} From 41203d8d69619f8f7935dbf822162e909b4feee6 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:44 +0300 Subject: [PATCH 38/60] Delete settings directory --- settings/Icon.tsx | 24 - settings/PropertyMapper.ts | 209 ---- settings/PropertyMapping.ts | 277 ----- settings/PropertyMappingModelComponent.tsx | 128 --- settings/Settings.ts | 1160 -------------------- settings/apiSecretsHelper.ts | 28 - settings/suggesters/FileSuggest.ts | 17 - settings/suggesters/FolderSuggest.ts | 17 - 8 files changed, 1860 deletions(-) delete mode 100644 settings/Icon.tsx delete mode 100644 settings/PropertyMapper.ts delete mode 100644 settings/PropertyMapping.ts delete mode 100644 settings/PropertyMappingModelComponent.tsx delete mode 100644 settings/Settings.ts delete mode 100644 settings/apiSecretsHelper.ts delete mode 100644 settings/suggesters/FileSuggest.ts delete mode 100644 settings/suggesters/FolderSuggest.ts diff --git a/settings/Icon.tsx b/settings/Icon.tsx deleted file mode 100644 index 97231973..00000000 --- a/settings/Icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { onMount, Show } from 'solid-js'; -import { setIcon } from 'obsidian'; - -interface IconProps { - iconName?: string; -} - -export default function Icon(props: IconProps) { - let iconEl: HTMLDivElement | undefined; - - onMount(() => { - if (iconEl) { - setIcon(iconEl, props.iconName || ''); - } - }); - - return ( - 0}> -
-
-
-
- ); -} diff --git a/settings/PropertyMapper.ts b/settings/PropertyMapper.ts deleted file mode 100644 index 1ff0c809..00000000 --- a/settings/PropertyMapper.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type MediaDbPlugin from '../main'; -import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; -import { PropertyMappingOption } from './PropertyMapping'; - -export class PropertyMapper { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - this.plugin = plugin; - } - - /** - * Converts an object using the conversion rules for its type. - * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. - * - * @param obj - */ - convertObject(obj: Record): Record { - if (!Object.hasOwn(obj, 'type')) { - return obj; - } - - // console.log(obj.type); - - const internalMediaType = resolveMetadataTypeToMediaType(this.plugin.settings, obj.type); - if (!internalMediaType) { - return obj; - } - - const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === internalMediaType); - if (!propertyMappingModel) { - return obj; - } - - const propertyMappings = propertyMappingModel.properties; - - const newObj: Record = {}; - - const entityProps = this.plugin.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s); - - // 1. Preprocess global wiki-links on the raw object first - if (this.plugin.settings.enableWikiLinkParsing && entityProps.length > 0) { - for (const [key, value] of Object.entries(obj)) { - if (key === 'aliases') continue; - if (entityProps.includes(key.toLowerCase())) { - const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; - const formatWiki = (v: unknown) => { - if (typeof v !== 'string') return v; - let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); - if (clean.includes('|')) clean = clean.split('|')[1]; - return `[[${folderPrefix}${clean}|${clean}]]`; - }; - - if (typeof value === 'string') { - obj[key] = formatWiki(value); - } else if (Array.isArray(value)) { - obj[key] = value.map(formatWiki); - } - } - } - } - - // 2. Map standard properties - for (const [key, value] of Object.entries(obj)) { - if (key === 'aliases') { - continue; - } - for (const propertyMapping of propertyMappings) { - if (propertyMapping.property === key) { - let finalValue = value; - if (propertyMapping.wikilink) { - const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; - // Resolve the originating API so it can provide property-specific link formatting - const api = typeof obj.dataSource === 'string' - ? this.plugin.apiManager.getApiByName(obj.dataSource) - : undefined; - const wikilink = (v: unknown): unknown => { - if (typeof v !== 'string') return v; - if (api) return api.wikilinkValueFor(key, v, obj, folderPrefix); - const clean = v.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; - return `[[${folderPrefix}${clean}|${clean}]]`; - }; - if (typeof value === 'string') { - finalValue = wikilink(value); - } else if (Array.isArray(value)) { - finalValue = value.map(wikilink); - } - } - if (propertyMapping.mapping === PropertyMappingOption.Map) { - // @ts-ignore - newObj[propertyMapping.newProperty] = finalValue; - } else if (propertyMapping.mapping === PropertyMappingOption.Remove) { - // do nothing - } else if (propertyMapping.mapping === PropertyMappingOption.Default) { - // @ts-ignore - newObj[key] = finalValue; - } - break; - } - } - } - - if (Object.hasOwn(obj, 'aliases')) { - const aliasesPm = propertyMappings.find(p => p.property === 'aliases'); - if (aliasesPm?.mapping !== PropertyMappingOption.Remove) { - const incoming = obj['aliases']; - const targetKey = - aliasesPm?.mapping === PropertyMappingOption.Map && aliasesPm.newProperty - ? aliasesPm.newProperty - : 'aliases'; - const merged = PropertyMapper.mergeAliasValues(newObj[targetKey], incoming); - if (merged.length > 0) { - newObj[targetKey] = merged; - } - } - } - - return newObj; - } - - private static mergeAliasValues(existing: unknown, added: unknown): string[] { - const toStrings = (v: unknown): string[] => { - if (v == null) { - return []; - } - if (Array.isArray(v)) { - return v.flatMap(x => (typeof x === 'string' ? x : String(x))).filter(s => s.length > 0); - } - if (typeof v === 'string') { - return v.length > 0 ? [v] : []; - } - return []; - }; - - const combined = [...toStrings(existing), ...toStrings(added)]; - const seen = new Set(); - const out: string[] = []; - for (const s of combined) { - if (!seen.has(s)) { - seen.add(s); - out.push(s); - } - } - return out; - } - - /** - * Converts an object back using the conversion rules for its type. - * Returns an unaltered object if object.type is null or undefined or if there are no conversion rules for the type. - * - * @param obj - */ - convertObjectBack(obj: Record): Record { - const models = this.plugin.settings.propertyMappingModels; - - let matchedModel: (typeof models)[number] | undefined; - for (const model of models) { - const typePm = model.properties.find(p => p.property === 'type'); - const typeKey = - typePm?.mapping === PropertyMappingOption.Map && typePm.newProperty - ? typePm.newProperty - : 'type'; - if (!Object.hasOwn(obj, typeKey)) { - continue; - } - let typeVal: unknown = obj[typeKey]; - if (typeVal === 'manga') { - typeVal = 'comicManga'; - console.debug(`MDB | updated metadata type`, typeVal); - } - const typeStr = String(typeVal).trim(); - if ( - typeStr === (model.type as string) || - typeStr === noteTypeValueForMedia(this.plugin.settings, model.type) - ) { - matchedModel = model; - break; - } - } - - if (!matchedModel) { - return obj; - } - - const propertyMappings = matchedModel.properties; - const originalObj: Record = {}; - - objLoop: for (const [key, value] of Object.entries(obj)) { - for (const propertyMapping of propertyMappings) { - if (propertyMapping.property === key) { - originalObj[key] = value; - continue objLoop; - } - } - for (const propertyMapping of propertyMappings) { - if ( - propertyMapping.mapping === PropertyMappingOption.Map && - propertyMapping.newProperty === key - ) { - originalObj[propertyMapping.property] = value; - continue objLoop; - } - } - } - - return originalObj; - } - -} diff --git a/settings/PropertyMapping.ts b/settings/PropertyMapping.ts deleted file mode 100644 index 649320e9..00000000 --- a/settings/PropertyMapping.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; -import type { MediaType } from '../utils/MediaType'; -import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; - -// Plain object interfaces for serialization -export interface PropertyMappingData { - property: string; - newProperty: string; - mapping: PropertyMappingOption; - locked?: boolean; - wikilink?: boolean; -} - -export interface PropertyMappingModelData { - type: MediaType; - properties: PropertyMappingData[]; -} - -export enum PropertyMappingOption { - Default = 'default', - Map = 'remap', - Remove = 'remove', -} - -export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; - -const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id'] as const; - -export class PropertyMappingModel { - type: MediaType; - properties: PropertyMapping[]; - - constructor(type: MediaType, properties?: PropertyMapping[]) { - this.type = type; - this.properties = properties ?? []; - } - - validate(): { res: boolean; err?: Error } { - console.debug(`MDB | validated property mappings for ${this.type}`); - - // check properties - for (const property of this.properties) { - const propertyValidation = property.validate(); - if (!propertyValidation.res) { - return { - res: false, - err: propertyValidation.err, - }; - } - } - - // check for name collisions - for (const property of this.getMappedProperties()) { - const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty); - if (propertiesWithSameTarget.length === 0) { - // if we get there, then something in this code is wrong - } else if (propertiesWithSameTarget.length === 1) { - // all good - } else { - // two or more properties are mapped to the same property - return { - res: false, - err: new PropertyMappingNameConflictError( - `Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`, - ), - }; - } - } - // remapped properties may not have the same name as any original property - for (const property of this.getMappedProperties()) { - const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property); - if (propertiesWithSameTarget.length === 0) { - // all good - } else { - // a mapped property shares the same name with an original property - return { - res: false, - err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`), - }; - } - } - - const dataSourceRule = this.properties.find(p => p.property === 'dataSource'); - if (dataSourceRule?.mapping === PropertyMappingOption.Remove && !musicBrainzRegisteredApiName(this.type)) { - return { - res: false, - err: new PropertyMappingValidationError( - `Removing dataSource is only allowed for artist, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, - ), - }; - } - - return { - res: true, - }; - } - - getMappedProperties(): PropertyMapping[] { - return this.properties.filter(x => x.mapping === PropertyMappingOption.Map); - } - - copy(): PropertyMappingModel { - const copy = new PropertyMappingModel(this.type); - for (const property of this.properties) { - const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked, property.wikilink); - copy.properties.push(propertyCopy); - } - return copy; - } - - // Serialization - returns a plain object that can be JSON.stringify'd - toJSON(): PropertyMappingModelData { - return { - type: this.type, - properties: this.properties.map(p => p.toJSON()), - }; - } - - // Deserialization - creates a PropertyMappingModel from a plain object - static fromJSON(json: PropertyMappingModelData): PropertyMappingModel { - return new PropertyMappingModel( - json.type, - json.properties.map(p => PropertyMapping.fromJSON(p)), - ); - } - - /** - * Migrates loaded settings to match the structure of default settings. - * - Adds new properties from defaults that don't exist in loaded settings - * - Preserves user customizations from loaded settings - * - Updates locked status from defaults - * - * @param loadedModels - Models loaded from disk (may be outdated) - * @param defaultModels - Current default models (source of truth for structure) - * @returns Migrated models with correct structure and preserved user settings - */ - static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { - const migratedModels: PropertyMappingModel[] = []; - - for (const defaultModel of defaultModels) { - const loadedModel = loadedModels.find(m => m.type === defaultModel.type); - - if (!loadedModel) { - // New model type - use default - migratedModels.push(defaultModel); - continue; - } - - // Migrate properties - const migratedProperties: PropertyMapping[] = []; - for (const defaultProperty of defaultModel.properties) { - const loadedProperty = loadedModel.properties.find(p => p.property === defaultProperty.property); - - if (!loadedProperty) { - // New property - use default - migratedProperties.push(defaultProperty); - } else { - // Existing property - merge: take locked from default, customizations from loaded - migratedProperties.push( - new PropertyMapping( - loadedProperty.property, - loadedProperty.newProperty, - loadedProperty.mapping, - defaultProperty.locked, // locked status from default - loadedProperty.wikilink ?? false, - ), - ); - } - } - - migratedModels.push(new PropertyMappingModel(defaultModel.type, migratedProperties)); - } - - return migratedModels; - } -} - -export class PropertyMapping { - property: string; - newProperty: string; - locked: boolean; - mapping: PropertyMappingOption; - wikilink: boolean; - - constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean) { - this.property = property; - this.newProperty = newProperty; - this.mapping = mapping; - this.locked = locked ?? false; - this.wikilink = wikilink ?? false; - } - - validate(): { res: boolean; err?: Error } { - // locked property may only be default - if (this.locked) { - if (this.mapping === PropertyMappingOption.Remove) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`), - }; - } - if (this.mapping === PropertyMappingOption.Map) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`), - }; - } - } - - if ( - (METADATA_KEYS_REQUIRED_IN_NOTE as readonly string[]).includes(this.property) && - this.mapping === PropertyMappingOption.Remove - ) { - return { - res: false, - err: new PropertyMappingValidationError( - `Error in property mapping "${this.toString()}": type and id must appear in the note (you can remap them, but not remove them).`, - ), - }; - } - - if (this.mapping === PropertyMappingOption.Default) { - return { res: true }; - } - if (this.mapping === PropertyMappingOption.Remove) { - return { res: true }; - } - - if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and may only contain letters and underscores.`), - }; - } - - if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) { - return { - res: false, - err: new PropertyMappingValidationError( - `Error in property mapping "${this.toString()}": new property may not be empty and may only contain letters and underscores.`, - ), - }; - } - - return { - res: true, - }; - } - - toString(): string { - if (this.mapping === PropertyMappingOption.Default) { - return this.property; - } else if (this.mapping === PropertyMappingOption.Map) { - return `${this.property} -> ${this.newProperty}`; - } else if (this.mapping === PropertyMappingOption.Remove) { - return `remove ${this.property}`; - } - - return this.property; - } - - // Serialization - returns a plain object - toJSON(): PropertyMappingData { - return { - property: this.property, - newProperty: this.newProperty, - mapping: this.mapping, - locked: this.locked, - wikilink: this.wikilink, - }; - } - - // Deserialization - creates a PropertyMapping from a plain object - static fromJSON(json: PropertyMappingData): PropertyMapping { - return new PropertyMapping(json.property, json.newProperty, json.mapping, json.locked, json.wikilink); - } -} diff --git a/settings/PropertyMappingModelComponent.tsx b/settings/PropertyMappingModelComponent.tsx deleted file mode 100644 index a533687a..00000000 --- a/settings/PropertyMappingModelComponent.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { createMemo, For, Show } from 'solid-js'; -import { createStore } from 'solid-js/store'; -import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; -import type { MediaType } from '../utils/MediaType'; -import { mediaTypeDisplayName } from '../utils/Utils'; -import Icon from './Icon'; - -interface PropertyMappingModelComponentProps { - model: PropertyMappingModelData; - save: (model: PropertyMappingModelData) => void; - /** When false, hides the media-type heading (e.g. modal title already shows it). Default true. */ - showMediaTypeTitle?: boolean; -} - -export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { - // Create a store from the model's plain data - const [modelData, setModelData] = createStore(props.model); - - // Derive the validation result reactively - const validationResult = createMemo(() => { - const model = PropertyMappingModel.fromJSON(modelData); - return model.validate(); - }); - - const persistIfValid = () => { - const model = PropertyMappingModel.fromJSON(modelData); - if (model.validate().res) { - props.save(model); - } - }; - - const showTitle = () => props.showMediaTypeTitle !== false; - - return ( -
- -
-
{mediaTypeDisplayName(modelData.type as MediaType)}
-
-
- - -
{validationResult().err?.message}
-
- -
- - - - - - - - - - - - {(property, index) => ( - - - - -
property cannot be remapped
- - } - > -
- - - - - - - )} - - -
PropertyMappingNew nameWikilink
- {property.property} - - - - —} - > -
- - { - setModelData('properties', index(), 'newProperty', e.currentTarget.value); - persistIfValid(); - }} - /> -
-
-
-
-
- ); -} diff --git a/settings/Settings.ts b/settings/Settings.ts deleted file mode 100644 index 8b62bb28..00000000 --- a/settings/Settings.ts +++ /dev/null @@ -1,1160 +0,0 @@ -import type { App, IconName } from 'obsidian'; -import { Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; -import { MediaType } from 'src/utils/MediaType'; -import type MediaDbPlugin from '../main'; -import { ApiSecretID } from './apiSecretsHelper'; -import { PropertyMappingModal } from '../modals/PropertyMappingModal'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import { MEDIA_TYPES } from '../utils/MediaTypeManager'; -import { noteTypeValueForMedia, setNoteTypeForMedia } from '../utils/noteTypeSettings'; -import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; -import type { PropertyMappingModelData } from './PropertyMapping'; -import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; -import { FileSuggest } from './suggesters/FileSuggest'; -import { FolderSuggest } from './suggesters/FolderSuggest'; - - -function mediaTypeTabIcon(mediaType: MediaType): IconName { - switch (mediaType) { - case MediaType.Artist: - return 'mic-2'; - case MediaType.BoardGame: - return 'dice-3'; - case MediaType.Book: - return 'book-marked'; - case MediaType.ComicManga: - return 'book-open'; - case MediaType.Game: - return 'gamepad-2'; - case MediaType.Movie: - return 'film'; - case MediaType.MusicRelease: - return 'disc-3'; - case MediaType.Season: - return 'calendar-range'; - case MediaType.Series: - return 'tv'; - case MediaType.Song: - return 'music-4'; - case MediaType.Wiki: - return 'library-big'; - } -} - -// MARK: Settings -export interface MediaDbPluginSettings { - sfwFilter: boolean; - templates: boolean; - customDateFormat: string; - openNoteInNewTab: boolean; - useDefaultFrontMatter: boolean; - /** When true, add an Obsidian `aliases` entry with an ASCII form of the title when it uses diacritics or letters like ø (e.g. Likbør → Likbor). */ - autoTrackerAiringKey: string; - autoTrackerReleasedKey: string; - enableTemplaterIntegration: boolean; - imageDownload: boolean; - imageFolder: string; - tmdbRegion: string; - enableAutoTagging: boolean; - autoTagEntities: string; - autoTagProperties: string; - enableWikiLinkParsing: boolean; - autoUpdateAiringMode: boolean; - - BoardgameGeekAPI_disabledMediaTypes: MediaType[]; - ComicVineAPI_disabledMediaTypes: MediaType[]; - GiantBombAPI_disabledMediaTypes: MediaType[]; - IGDBAPI_disabledMediaTypes: MediaType[]; - RAWGAPI_disabledMediaTypes: MediaType[]; - MALAPI_disabledMediaTypes: MediaType[]; - MALAPIManga_disabledMediaTypes: MediaType[]; - MobyGamesAPI_disabledMediaTypes: MediaType[]; - MusicBrainzAPI_disabledMediaTypes: MediaType[]; - MusicBrainzArtistAPI_disabledMediaTypes: MediaType[]; - OMDbAPI_disabledMediaTypes: MediaType[]; - OpenLibraryAPI_disabledMediaTypes: MediaType[]; - SteamAPI_disabledMediaTypes: MediaType[]; - TMDBMovieAPI_disabledMediaTypes: MediaType[]; - TMDBSeasonAPI_disabledMediaTypes: MediaType[]; - TMDBSeriesAPI_disabledMediaTypes: MediaType[]; - VNDBAPI_disabledMediaTypes: MediaType[]; - WikipediaAPI_disabledMediaTypes: MediaType[]; - - movieTemplate: string; - seriesTemplate: string; - seasonTemplate: string; - mangaTemplate: string; - gameTemplate: string; - wikiTemplate: string; - musicReleaseTemplate: string; - artistTemplate: string; - songTemplate: string; - boardgameTemplate: string; - bookTemplate: string; - - movieFileNameTemplate: string; - seriesFileNameTemplate: string; - seasonFileNameTemplate: string; - mangaFileNameTemplate: string; - gameFileNameTemplate: string; - wikiFileNameTemplate: string; - musicReleaseFileNameTemplate: string; - artistFileNameTemplate: string; - songFileNameTemplate: string; - boardgameFileNameTemplate: string; - bookFileNameTemplate: string; - - movieFolder: string; - seriesFolder: string; - seasonFolder: string; - mangaFolder: string; - gameFolder: string; - wikiFolder: string; - musicReleaseFolder: string; - artistFolder: string; - songFolder: string; - - /** Frontmatter `type` for each media kind (empty = default internal id, e.g. movie, musicRelease). */ - movieNoteType: string; - seriesNoteType: string; - seasonNoteType: string; - mangaNoteType: string; - gameNoteType: string; - wikiNoteType: string; - musicReleaseNoteType: string; - artistNoteType: string; - songNoteType: string; - boardgameNoteType: string; - bookNoteType: string; - /** When true, artist discography import nests albums and songs under artistFolder/ArtistName/… instead of using album/song import folders. */ - artistUseFileTreeForSongs: boolean; - boardgameFolder: string; - bookFolder: string; - - propertyMappingModels: PropertyMappingModelData[]; - linkedApiSecretIds: Record; -} - -/** - * Helper class to get/set settings for a specific media type. - */ -class MediaTypeMappedSettings { - mediaType: MediaType; - - constructor(mediaType: MediaType) { - this.mediaType = mediaType; - } - - getTemplate(settings: MediaDbPluginSettings): string { - switch (this.mediaType) { - case MediaType.Artist: - return settings.artistTemplate; - case MediaType.BoardGame: - return settings.boardgameTemplate; - case MediaType.Book: - return settings.bookTemplate; - case MediaType.ComicManga: - return settings.mangaTemplate; - case MediaType.Game: - return settings.gameTemplate; - case MediaType.Movie: - return settings.movieTemplate; - case MediaType.MusicRelease: - return settings.musicReleaseTemplate; - case MediaType.Season: - return settings.seasonTemplate; - case MediaType.Series: - return settings.seriesTemplate; - case MediaType.Song: - return settings.songTemplate; - case MediaType.Wiki: - return settings.wikiTemplate; - } - } - - setTemplate(settings: MediaDbPluginSettings, template: string): void { - switch (this.mediaType) { - case MediaType.Artist: - settings.artistTemplate = template; - break; - case MediaType.BoardGame: - settings.boardgameTemplate = template; - break; - case MediaType.Book: - settings.bookTemplate = template; - break; - case MediaType.ComicManga: - settings.mangaTemplate = template; - break; - case MediaType.Game: - settings.gameTemplate = template; - break; - case MediaType.Movie: - settings.movieTemplate = template; - break; - case MediaType.MusicRelease: - settings.musicReleaseTemplate = template; - break; - case MediaType.Season: - settings.seasonTemplate = template; - break; - case MediaType.Series: - settings.seriesTemplate = template; - break; - case MediaType.Song: - settings.songTemplate = template; - break; - case MediaType.Wiki: - settings.wikiTemplate = template; - break; - } - } - - getFileNameTemplate(settings: MediaDbPluginSettings): string { - switch (this.mediaType) { - case MediaType.Artist: - return settings.artistFileNameTemplate; - case MediaType.BoardGame: - return settings.boardgameFileNameTemplate; - case MediaType.Book: - return settings.bookFileNameTemplate; - case MediaType.ComicManga: - return settings.mangaFileNameTemplate; - case MediaType.Game: - return settings.gameFileNameTemplate; - case MediaType.Movie: - return settings.movieFileNameTemplate; - case MediaType.MusicRelease: - return settings.musicReleaseFileNameTemplate; - case MediaType.Season: - return settings.seasonFileNameTemplate; - case MediaType.Series: - return settings.seriesFileNameTemplate; - case MediaType.Song: - return settings.songFileNameTemplate; - case MediaType.Wiki: - return settings.wikiFileNameTemplate; - } - } - - setFileNameTemplate(settings: MediaDbPluginSettings, template: string): void { - switch (this.mediaType) { - case MediaType.Artist: - settings.artistFileNameTemplate = template; - break; - case MediaType.BoardGame: - settings.boardgameFileNameTemplate = template; - break; - case MediaType.Book: - settings.bookFileNameTemplate = template; - break; - case MediaType.ComicManga: - settings.mangaFileNameTemplate = template; - break; - case MediaType.Game: - settings.gameFileNameTemplate = template; - break; - case MediaType.Movie: - settings.movieFileNameTemplate = template; - break; - case MediaType.MusicRelease: - settings.musicReleaseFileNameTemplate = template; - break; - case MediaType.Season: - settings.seasonFileNameTemplate = template; - break; - case MediaType.Series: - settings.seriesFileNameTemplate = template; - break; - case MediaType.Song: - settings.songFileNameTemplate = template; - break; - case MediaType.Wiki: - settings.wikiFileNameTemplate = template; - break; - } - } - - getFolder(settings: MediaDbPluginSettings): string { - switch (this.mediaType) { - case MediaType.Artist: - return settings.artistFolder; - case MediaType.BoardGame: - return settings.boardgameFolder; - case MediaType.Book: - return settings.bookFolder; - case MediaType.ComicManga: - return settings.mangaFolder; - case MediaType.Game: - return settings.gameFolder; - case MediaType.Movie: - return settings.movieFolder; - case MediaType.MusicRelease: - return settings.musicReleaseFolder; - case MediaType.Season: - return settings.seasonFolder; - case MediaType.Series: - return settings.seriesFolder; - case MediaType.Song: - return settings.songFolder; - case MediaType.Wiki: - return settings.wikiFolder; - } - } - - setFolder(settings: MediaDbPluginSettings, folder: string): void { - switch (this.mediaType) { - case MediaType.Artist: - settings.artistFolder = folder; - break; - case MediaType.BoardGame: - settings.boardgameFolder = folder; - break; - case MediaType.Book: - settings.bookFolder = folder; - break; - case MediaType.ComicManga: - settings.mangaFolder = folder; - break; - case MediaType.Game: - settings.gameFolder = folder; - break; - case MediaType.Movie: - settings.movieFolder = folder; - break; - case MediaType.MusicRelease: - settings.musicReleaseFolder = folder; - break; - case MediaType.Season: - settings.seasonFolder = folder; - break; - case MediaType.Series: - settings.seriesFolder = folder; - break; - case MediaType.Song: - settings.songFolder = folder; - break; - case MediaType.Wiki: - settings.wikiFolder = folder; - break; - } - } - - getNoteType(settings: MediaDbPluginSettings): string { - const configured = noteTypeValueForMedia(settings, this.mediaType); - return configured === this.mediaType ? '' : configured; - } - - setNoteType(settings: MediaDbPluginSettings, value: string): void { - const trimmed = value.trim(); - if (trimmed === '' || trimmed === this.mediaType) { - setNoteTypeForMedia(settings, this.mediaType, ''); - return; - } - setNoteTypeForMedia(settings, this.mediaType, value); - } -} - -// MARK: Defaults -const DEFAULT_SETTINGS: MediaDbPluginSettings = { - sfwFilter: true, - templates: true, - customDateFormat: 'L', - openNoteInNewTab: true, - useDefaultFrontMatter: true, - autoTrackerAiringKey: 'airing', - autoTrackerReleasedKey: 'released', - enableTemplaterIntegration: false, - imageDownload: false, - imageFolder: 'Media DB/images', - enableAutoTagging: false, - autoTagEntities: '', - autoTagProperties: '', - enableWikiLinkParsing: false, - autoUpdateAiringMode: false, - tmdbRegion: 'US', - - BoardgameGeekAPI_disabledMediaTypes: [], - ComicVineAPI_disabledMediaTypes: [], - GiantBombAPI_disabledMediaTypes: [], - IGDBAPI_disabledMediaTypes: [], - RAWGAPI_disabledMediaTypes: [], - MALAPI_disabledMediaTypes: [], - MALAPIManga_disabledMediaTypes: [], - MobyGamesAPI_disabledMediaTypes: [], - MusicBrainzAPI_disabledMediaTypes: [], - MusicBrainzArtistAPI_disabledMediaTypes: [], - OMDbAPI_disabledMediaTypes: [], - OpenLibraryAPI_disabledMediaTypes: [], - SteamAPI_disabledMediaTypes: [], - TMDBMovieAPI_disabledMediaTypes: [], - TMDBSeasonAPI_disabledMediaTypes: [], - TMDBSeriesAPI_disabledMediaTypes: [], - VNDBAPI_disabledMediaTypes: [], - WikipediaAPI_disabledMediaTypes: [], - - movieTemplate: '', - seriesTemplate: '', - seasonTemplate: '', - mangaTemplate: '', - gameTemplate: '', - wikiTemplate: '', - musicReleaseTemplate: '', - artistTemplate: '', - songTemplate: '', - boardgameTemplate: '', - bookTemplate: '', - - movieFileNameTemplate: '{{ title }} ({{ year }})', - seriesFileNameTemplate: '{{ title }} ({{ year }})', - seasonFileNameTemplate: '{{ title }} ({{ year }})', - mangaFileNameTemplate: '{{ title }} ({{ year }})', - gameFileNameTemplate: '{{ title }} ({{ year }})', - wikiFileNameTemplate: '{{ title }}', - musicReleaseFileNameTemplate: '{{ title }} ({{ FIRST:artists }} - {{ year }})', - artistFileNameTemplate: '{{ title }}', - songFileNameTemplate: '{{ trackNumber }}. {{ title }} ({{ albumTitle }})', - boardgameFileNameTemplate: '{{ title }} ({{ year }})', - bookFileNameTemplate: '{{ title }} ({{ year }})', - - movieFolder: 'Media DB/movies', - seriesFolder: 'Media DB/series', - seasonFolder: 'Media DB/series', - mangaFolder: 'Media DB/comics', - gameFolder: 'Media DB/games', - wikiFolder: 'Media DB/wiki', - musicReleaseFolder: 'Media DB/music', - artistFolder: 'Media DB/artists', - songFolder: 'Media DB/music/songs', - artistUseFileTreeForSongs: false, - boardgameFolder: 'Media DB/boardgames', - bookFolder: 'Media DB/books', - - movieNoteType: '', - seriesNoteType: '', - seasonNoteType: '', - mangaNoteType: '', - gameNoteType: '', - wikiNoteType: '', - musicReleaseNoteType: '', - artistNoteType: '', - songNoteType: '', - boardgameNoteType: '', - bookNoteType: '', - - propertyMappingModels: [], - - linkedApiSecretIds: { - [ApiSecretID.omdb]: '', - [ApiSecretID.tmdb]: '', - [ApiSecretID.mobyGames]: '', - [ApiSecretID.giantBomb]: '', - [ApiSecretID.igdbClientId]: '', - [ApiSecretID.igdbClientSecret]: '', - [ApiSecretID.rawg]: '', - [ApiSecretID.comicVine]: '', - [ApiSecretID.boardgameGeek]: '', - [ApiSecretID.genius]: '', - [ApiSecretID.spotifyClientId]: '', - [ApiSecretID.spotifyClientSecret]: '', - }, -}; - -export const lockedPropertyMappings: string[] = []; - -export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings { - const defaultSettings = DEFAULT_SETTINGS; - - // construct property mapping defaults - const propertyMappingModels: PropertyMappingModelData[] = []; - for (const mediaType of MEDIA_TYPES) { - const model: MediaTypeModel = plugin.mediaTypeManager.createMediaTypeModelFromMediaType({}, mediaType); - const metadataObj = model.toMetaDataObject(); - - const propertyMappingModel: PropertyMappingModel = new PropertyMappingModel(mediaType); - - for (const key of Object.keys(metadataObj)) { - propertyMappingModel.properties.push( - new PropertyMapping( - key, - '', - PropertyMappingOption.Default, - lockedPropertyMappings.contains(key), - false, // wikilink default - ), - ); - } - - // Convert to plain data for serialization - propertyMappingModels.push(propertyMappingModel.toJSON()); - } - - defaultSettings.propertyMappingModels = propertyMappingModels; - return defaultSettings; -} - -interface MediaDbSettingsTabNavEntry { - id: string; - nav: HTMLElement; - panel: HTMLElement; -} - -/** Stable order for property-mapping UI and persisted settings (`MEDIA_TYPES`; settings tabs move Board game last). */ -export function propertyMappingModelsInDisplayOrder(models: PropertyMappingModelData[]): PropertyMappingModelData[] { - const order = new Map(MEDIA_TYPES.map((t, i) => [t, i])); - return [...models].sort((a, b) => (order.get(a.type) ?? 999) - (order.get(b.type) ?? 999)); -} - -// MARK: Settings Tab -export class MediaDbSettingTab extends PluginSettingTab { - plugin: MediaDbPlugin; - private activeSettingsTabId: string | null = null; - - constructor(app: App, plugin: MediaDbPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - private addApiSecretSetting(group: SettingGroup, name: string, description: string, slot: ApiSecretID): void { - group.addSetting( - setting => - setting - .setName(name) - .setDesc(description) - .addComponent(el => { - const component = new SecretComponent(this.app, el); - const { linkedApiSecretIds } = this.plugin.settings; - const linkedId = linkedApiSecretIds[slot] ?? ''; - component.setValue(linkedId).onChange((secretId: string) => { - linkedApiSecretIds[slot] = secretId; - this.plugin.saveSettings(); - }); - return component; - }), - ); - } - - private static readonly MUSIC_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Artist, MediaType.MusicRelease, MediaType.Song]; - - private static readonly BOOK_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Book, MediaType.ComicManga]; - - private static readonly VIDEO_SETTINGS_MEDIA_TYPES: readonly MediaType[] = [MediaType.Movie, MediaType.Series, MediaType.Season]; - - private renderMediaTypeSection( - panel: HTMLElement, - mediaTypeSetting: MediaTypeMappedSettings, - mediaTypeApiMap: Map, - options?: { - sectionHeading?: string; - hideImportFolder?: boolean; - appendToSection?: (group: SettingGroup) => void; - }, - ): void { - const mediaType = mediaTypeSetting.mediaType; - const descNoun = options?.sectionHeading?.toLowerCase() ?? mediaTypeDisplayName(mediaType).toLowerCase(); - - if (options?.sectionHeading) { - panel.createEl('h3', { text: options.sectionHeading }); - } - - const mediaTypeGroup = new SettingGroup(panel); - - if (!options?.hideImportFolder) { - mediaTypeGroup.addSetting( - setting => - void setting - .setName('Import folder') - .setDesc(`Where newly imported ${descNoun} notes should be placed.`) - .addSearch(cb => { - const suggester = new FolderSuggest(this.app, cb.inputEl); - suggester.onSelect(folder => { - cb.setValue(folder.path); - mediaTypeSetting.setFolder(this.plugin.settings, folder.path); - void this.plugin.saveSettings(); - suggester.close(); - }); - cb.setPlaceholder(mediaTypeSetting.getFolder(DEFAULT_SETTINGS)) - .setValue(mediaTypeSetting.getFolder(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setFolder(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); - }), - ); - } - - mediaTypeGroup.addSetting( - setting => - void setting - .setName('Note type') - .setDesc( - `Value for the "type" field in frontmatter. Leave blank to use the default (${mediaType}).`, - ) - .addText(cb => { - cb.setPlaceholder(String(mediaType)) - .setValue(mediaTypeSetting.getNoteType(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setNoteType(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); - }), - ); - - mediaTypeGroup.addSetting( - setting => - void setting - .setName('Template') - .setDesc(`Template file used when creating a new ${descNoun} note.`) - .addSearch(cb => { - const suggester = new FileSuggest(this.app, cb.inputEl); - suggester.onSelect(file => { - cb.setValue(file.path); - mediaTypeSetting.setTemplate(this.plugin.settings, file.path); - void this.plugin.saveSettings(); - suggester.close(); - }); - cb.setPlaceholder(`Example: ${descNoun.replace(/ /g, '')}Template.md`) - .setValue(mediaTypeSetting.getTemplate(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setTemplate(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); - }), - ); - - mediaTypeGroup.addSetting( - setting => - void setting - .setName('File name template') - .setDesc(`File name template for new ${descNoun} notes.`) - .addText(cb => { - cb.setPlaceholder(`Example: ${mediaTypeSetting.getFileNameTemplate(DEFAULT_SETTINGS)}`) - .setValue(mediaTypeSetting.getFileNameTemplate(this.plugin.settings)) - .onChange(data => { - mediaTypeSetting.setFileNameTemplate(this.plugin.settings, data); - void this.plugin.saveSettings(); - }); - }), - ); - - const apis = mediaTypeApiMap.get(mediaType) ?? []; - if (apis.length > 1) { - for (const apiName of apis) { - const api = this.plugin.apiManager.apis.find(a => a.apiName === apiName); - if (api) { - const disabledMediaTypes = api.getDisabledMediaTypes(); - - mediaTypeGroup.addSetting( - setting => - void setting - .setName(apiName) - .setDesc(`Use ${apiName} for ${descNoun} search and import.`) - .addToggle(cb => { - cb.setValue(!disabledMediaTypes.includes(mediaType)).onChange(data => { - if (data) { - const index = disabledMediaTypes.indexOf(mediaType); - if (index != -1) { - disabledMediaTypes.splice(index, 1); - } - } else { - disabledMediaTypes.push(mediaType); - } - void this.plugin.saveSettings(); - }); - }), - ); - } - } - } - - options?.appendToSection?.(mediaTypeGroup); - - if (this.plugin.settings.useDefaultFrontMatter) { - mediaTypeGroup.addSetting(setting => - void setting - .setName('Property mappings') - .setDesc(`How metadata fields map to frontmatter for ${descNoun} notes.`) - .addButton(btn => { - btn.setButtonText('Edit'); - btn.onClick(() => { - new PropertyMappingModal(this.app, this.plugin, mediaType).open(); - }); - }), - ); - } - } - - private renderMusicSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { - const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; - const fileTree = this.plugin.settings.artistUseFileTreeForSongs; - - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - - this.renderMediaTypeSection(panel, byType(MediaType.Artist), mediaTypeApiMap, { - sectionHeading: 'Artist', - appendToSection: group => { - group.addSetting( - setting => - void setting - .setName('Use file trees for songs') - .setDesc( - 'Use a file tree hierarchy to store albums and songs for each artist.', - ) - .addToggle(cb => { - cb.setValue(this.plugin.settings.artistUseFileTreeForSongs).onChange(data => { - this.plugin.settings.artistUseFileTreeForSongs = data; - void this.plugin.saveSettings(); - this.display(); - }); - }), - ); - }, - }); - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { - sectionHeading: 'Album', - hideImportFolder: fileTree, - }); - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { - sectionHeading: 'Song', - hideImportFolder: fileTree, - }); - } - - private renderBookSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { - const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; - - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - - this.renderMediaTypeSection(panel, byType(MediaType.Book), mediaTypeApiMap, { - sectionHeading: 'Book', - }); - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.ComicManga), mediaTypeApiMap, { - sectionHeading: 'Comic & Manga', - }); - } - - private renderVideoSettingsTab(panel: HTMLElement, mediaTypeSettings: MediaTypeMappedSettings[], mediaTypeApiMap: Map): void { - const byType = (mt: MediaType): MediaTypeMappedSettings => mediaTypeSettings.find(s => s.mediaType === mt)!; - - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - - this.renderMediaTypeSection(panel, byType(MediaType.Movie), mediaTypeApiMap, { - sectionHeading: 'Movie', - }); - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Series), mediaTypeApiMap, { - sectionHeading: 'Series', - }); - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - this.renderMediaTypeSection(panel, byType(MediaType.Season), mediaTypeApiMap, { - sectionHeading: 'Season', - }); - - panel.createDiv({ cls: 'media-db-plugin-spacer' }); - panel.createEl('h3', { text: 'Region' }); - const regionGroup = new SettingGroup(panel); - regionGroup.addSetting( - setting => - void setting - .setName('TMDB Region') - .setDesc('ISO-3166-1 region code for TMDB localized metadata (e.g., US, TR, GB). Default is US.') - .addText(text => - text - .setPlaceholder('US') - .setValue(this.plugin.settings.tmdbRegion) - .onChange(async value => { - this.plugin.settings.tmdbRegion = value; - await this.plugin.saveSettings(); - }), - ), - ); - } - - display(): void { - const { containerEl } = this; - containerEl.empty(); - - const headerNav = containerEl.createEl('nav', { cls: 'media-db-setting-header' }); - const tabGroup = headerNav.createDiv({ cls: 'media-db-setting-tab-group' }); - const settingsContentEl = containerEl.createDiv({ cls: 'media-db-setting-content' }); - - const tabEntries: MediaDbSettingsTabNavEntry[] = []; - - const selectTab = (id: string): void => { - for (const { id: tid, nav, panel } of tabEntries) { - const on = tid === id; - panel.toggleClass('media-db-tab-settings--hidden', !on); - nav.toggleClass('media-db-navigation-item-selected', on); - } - this.activeSettingsTabId = id; - }; - - const addTab = (id: string, title: string, icon: IconName, render: (panel: HTMLElement) => void): void => { - const nav = tabGroup.createDiv({ cls: 'media-db-navigation-item' }); - nav.addClass(Platform.isMobile ? 'media-db-mobile' : 'media-db-desktop'); - setIcon(nav.createSpan({ cls: 'media-db-navigation-item-icon' }), icon); - nav.createSpan().setText(title); - const panel = settingsContentEl.createDiv({ cls: 'media-db-tab-settings media-db-tab-settings--hidden' }); - render(panel); - tabEntries.push({ id, nav, panel }); - nav.addEventListener('click', () => selectTab(id)); - }; - - const mediaTypeSettings = [ - ...MEDIA_TYPES.filter(mt => mt !== MediaType.BoardGame).map(mt => new MediaTypeMappedSettings(mt)), - new MediaTypeMappedSettings(MediaType.BoardGame), - ]; - - const mediaTypeApiMap = new Map(); - for (const api of this.plugin.apiManager.apis) { - for (const mediaType of api.types) { - if (!mediaTypeApiMap.has(mediaType)) { - mediaTypeApiMap.set(mediaType, []); - } - mediaTypeApiMap.get(mediaType)!.push(api.apiName); - } - } - - addTab('general', 'General', 'sliders-horizontal', panel => { - const generalGroup = new SettingGroup(panel); - - generalGroup.addSetting( - setting => - void setting - .setName('SFW filter') - .setDesc('Only shows SFW results for APIs that offer filtering.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.sfwFilter).onChange(data => { - this.plugin.settings.sfwFilter = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Resolve {{ tags }} in templates') - .setDesc('Whether to resolve {{ tags }} in templates. The spaces inside the curly braces are important.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.templates).onChange(data => { - this.plugin.settings.templates = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Date format') - .setDesc( - fragWithHTML( - "Your custom date format. Use 'YYYY-MM-DD' for example.
" + - "For more syntax, refer to format reference.
" + - "Your current syntax looks like this: " + - this.plugin.dateFormatter.getPreview() + - "", - ), - ) - .addText(cb => { - cb.setPlaceholder(DEFAULT_SETTINGS.customDateFormat) - .setValue(this.plugin.settings.customDateFormat === DEFAULT_SETTINGS.customDateFormat ? '' : this.plugin.settings.customDateFormat) - .onChange(data => { - const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; - this.plugin.settings.customDateFormat = newDateFormat; - const previewEl = document.getElementById('media-db-dateformat-preview'); - if (previewEl) { - previewEl.textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview - } - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Open note in new tab') - .setDesc('Open the newly created note in a new tab.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => { - this.plugin.settings.openNoteInNewTab = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Use default front matter') - .setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { - this.plugin.settings.useDefaultFrontMatter = data; - void this.plugin.saveSettings(); - this.display(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Enable Templater integration') - .setDesc('Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { - this.plugin.settings.enableTemplaterIntegration = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Download images') - .setDesc('Downloads images for new notes in the folder below') - .addToggle(cb => { - cb.setValue(this.plugin.settings.imageDownload).onChange(data => { - this.plugin.settings.imageDownload = data; - void this.plugin.saveSettings(); - }); - }), - ); - - generalGroup.addSetting( - setting => - void setting - .setName('Image folder') - .setDesc('Where downloaded images should be stored.') - .addSearch(cb => { - const suggester = new FolderSuggest(this.app, cb.inputEl); - suggester.onSelect(folder => { - cb.setValue(folder.path); - this.plugin.settings.imageFolder = folder.path; - void this.plugin.saveSettings(); - suggester.close(); - }); - cb.setPlaceholder(DEFAULT_SETTINGS.imageFolder) - .setValue(this.plugin.settings.imageFolder) - .onChange(data => { - this.plugin.settings.imageFolder = data; - void this.plugin.saveSettings(); - }); - }), - ); - - panel.createEl('h3', { text: 'Auto-Tracker' }).style.marginTop = '1.5em'; - const autoTrackerGroup = new SettingGroup(panel); - - autoTrackerGroup.addSetting( - setting => - void setting - .setName('Auto-Update Airing & Unreleased Media') - .setDesc('At startup, automatically searches background for any active medias with "released: false" or "airing: true" and updates them via API.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.autoUpdateAiringMode).onChange(data => { - this.plugin.settings.autoUpdateAiringMode = data; - void this.plugin.saveSettings(); - }); - }), - ); - - autoTrackerGroup.addSetting( - setting => - void setting - .setName('Auto-Tracker "Airing" Property') - .setDesc('Property key to check if a media item is currently airing. Default is "airing".') - .addText(text => { - text.setValue(this.plugin.settings.autoTrackerAiringKey).onChange(data => { - this.plugin.settings.autoTrackerAiringKey = data.trim() || 'airing'; - void this.plugin.saveSettings(); - }); - }), - ); - - autoTrackerGroup.addSetting( - setting => - void setting - .setName('Auto-Tracker "Released" Property') - .setDesc('Property key to check if a media item is unreleased. Default is "released".') - .addText(text => { - text.setValue(this.plugin.settings.autoTrackerReleasedKey).onChange(data => { - this.plugin.settings.autoTrackerReleasedKey = data.trim() || 'released'; - void this.plugin.saveSettings(); - }); - }), - ); - - panel.createEl('h3', { text: 'Auto-Tag Properties' }).style.marginTop = '1.5em'; - const autoTagGroup = new SettingGroup(panel); - - autoTagGroup.addSetting( - setting => - void setting - .setName('Enable Auto Tagging') - .setDesc('Feature to automatically sanitize properties into standard Obsidian tags.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.enableAutoTagging).onChange(data => { - this.plugin.settings.enableAutoTagging = data; - void this.plugin.saveSettings(); - }); - }), - ); - - autoTagGroup.addSetting( - setting => - void setting - .setName('Auto-Tag whitelisted properties') - .setDesc('Comma separated list of property names. If a property in this list is present, its values will be sanitized and appended to the Obsidian native `tags` array.') - .addText(text => - text - .setPlaceholder('genres, platforms') - .setValue(this.plugin.settings.autoTagProperties) - .onChange(async value => { - this.plugin.settings.autoTagProperties = value; - await this.plugin.saveSettings(); - }), - ), - ); - }); - - // Render individual media type tabs - // Game tab - const renderGameTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { - this.renderMediaTypeSection(panel, setting, mediaTypeApiMap); - }; - - // Wiki tab — inject Wiki-Link settings - const renderWikiTab = (panel: HTMLElement, setting: MediaTypeMappedSettings) => { - this.renderMediaTypeSection(panel, setting, mediaTypeApiMap, { - appendToSection: group => { - group.addSetting( - s => - void s - .setName('Wiki-Link parsing') - .setDesc('When enabled, properties listed below are formatted as Obsidian [[Wiki-Links]] across ALL media types globally. This complements the per-property wikilink checkbox in Property Mappings, which only affects that specific property.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.enableWikiLinkParsing).onChange(data => { - this.plugin.settings.enableWikiLinkParsing = data; - void this.plugin.saveSettings(); - }); - }), - ); - group.addSetting( - s => - void s - .setName('Wiki-Link properties') - .setDesc('Comma-separated property names to convert to [[Wiki-Links]] for ALL media types. Use this for custom or cross-type properties (e.g. storefront, launcher). For standard properties like genres or studio, the wikilink checkbox inside Property Mappings also works.') - .addTextArea(cb => { - cb.setPlaceholder('genres, storefront, category') - .setValue(this.plugin.settings.autoTagEntities) - .onChange(value => { - this.plugin.settings.autoTagEntities = value; - void this.plugin.saveSettings(); - }); - }), - ); - }, - }); - }; - - addTab('api-keys', 'API keys', 'key', panel => { - const apiKeyGroup = new SettingGroup(panel); - - this.addApiSecretSetting(apiKeyGroup, 'OMDb API key', 'API key for "www.omdbapi.com".', ApiSecretID.omdb); - this.addApiSecretSetting(apiKeyGroup, 'TMDB API Token', 'API Read Access Token for "https://www.themoviedb.org".', ApiSecretID.tmdb); - - this.addApiSecretSetting(apiKeyGroup, 'Moby Games key', 'API key for "www.mobygames.com".', ApiSecretID.mobyGames); - this.addApiSecretSetting(apiKeyGroup, 'Giant Bomb Key', 'API key for "www.giantbomb.com".', ApiSecretID.giantBomb); - this.addApiSecretSetting(apiKeyGroup, 'IGDB Client ID', 'Client ID for IGDB API (Required for Twitch OAuth).', ApiSecretID.igdbClientId); - this.addApiSecretSetting(apiKeyGroup, 'IGDB Client Secret', 'Client Secret for IGDB API.', ApiSecretID.igdbClientSecret); - this.addApiSecretSetting(apiKeyGroup, 'RAWG API Key', 'API key for "rawg.io".', ApiSecretID.rawg); - this.addApiSecretSetting(apiKeyGroup, 'Comic Vine Key', 'API key for "www.comicvine.gamespot.com".', ApiSecretID.comicVine); - this.addApiSecretSetting(apiKeyGroup, 'Boardgame Geek Key', 'API key for "www.boardgamegeek.com".', ApiSecretID.boardgameGeek); - this.addApiSecretSetting( - apiKeyGroup, - 'Genius API access token', - 'Client access token from https://genius.com/api-clients — used to search songs and load lyrics when importing an artist.', - ApiSecretID.genius, - ); - this.addApiSecretSetting( - apiKeyGroup, - 'Spotify Client ID', - 'From https://developer.spotify.com/dashboard — used to resolve track links when MusicBrainz has no Spotify URL (with Client Secret).', - ApiSecretID.spotifyClientId, - ); - this.addApiSecretSetting( - apiKeyGroup, - 'Spotify Client Secret', - 'Pair with Spotify Client ID for client-credentials access to search tracks during artist import.', - ApiSecretID.spotifyClientSecret, - ); - }); - - let musicTabAdded = false; - let bookTabAdded = false; - let videoTabAdded = false; - for (const mediaTypeSetting of mediaTypeSettings) { - const mediaType = mediaTypeSetting.mediaType; - - if (MediaDbSettingTab.MUSIC_SETTINGS_MEDIA_TYPES.includes(mediaType)) { - if (!musicTabAdded) { - musicTabAdded = true; - addTab('media-music', 'Music', 'disc-3', panel => { - this.renderMusicSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); - }); - } - continue; - } - - if (MediaDbSettingTab.BOOK_SETTINGS_MEDIA_TYPES.includes(mediaType)) { - if (!bookTabAdded) { - bookTabAdded = true; - addTab('media-book', 'Book', mediaTypeTabIcon(MediaType.Book), panel => { - this.renderBookSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); - }); - } - continue; - } - - if (MediaDbSettingTab.VIDEO_SETTINGS_MEDIA_TYPES.includes(mediaType)) { - if (!videoTabAdded) { - videoTabAdded = true; - addTab('media-movie', 'Movie', mediaTypeTabIcon(MediaType.Movie), panel => { - this.renderVideoSettingsTab(panel, mediaTypeSettings, mediaTypeApiMap); - }); - } - continue; - } - - const mediaTypeName = unCamelCase(mediaTypeSetting.mediaType); - if (mediaType === MediaType.Game) { - addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { - renderGameTab(panel, mediaTypeSetting); - }); - } else if (mediaType === MediaType.Wiki) { - addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { - renderWikiTab(panel, mediaTypeSetting); - }); - } else { - addTab(`media-${mediaType}`, mediaTypeName, mediaTypeTabIcon(mediaType), panel => { - this.renderMediaTypeSection(panel, mediaTypeSetting, mediaTypeApiMap); - }); - } - } - - const validIds = new Set(tabEntries.map(t => t.id)); - let initialId = this.activeSettingsTabId && validIds.has(this.activeSettingsTabId) ? this.activeSettingsTabId : 'general'; - if (!validIds.has(initialId)) { - initialId = 'general'; - } - selectTab(initialId); - } -} diff --git a/settings/apiSecretsHelper.ts b/settings/apiSecretsHelper.ts deleted file mode 100644 index b20908b7..00000000 --- a/settings/apiSecretsHelper.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { App } from 'obsidian'; - -/** - * Settings slots for API credentials. Each slot stores the Obsidian SecretStorage **id** - * the user selects (including via SecretComponent → Link), not the raw secret. - * @see https://docs.obsidian.md/plugins/guides/secret-storage - */ -export enum ApiSecretID { - omdb, - tmdb, - mobyGames, - giantBomb, - igdbClientId, - igdbClientSecret, - rawg, - comicVine, - boardgameGeek, - genius, - /** Spotify Developer Dashboard — used when MusicBrainz has no streaming URL for a recording. */ - spotifyClientId, - spotifyClientSecret, -} - -export function getApiSecretValue(app: App, linked: Record | undefined, slot: ApiSecretID): string { - const id = linked?.[slot] ?? ''; - if (id === '') return ''; - return app.secretStorage.getSecret(id) ?? ''; -} diff --git a/settings/suggesters/FileSuggest.ts b/settings/suggesters/FileSuggest.ts deleted file mode 100644 index 6bbdb968..00000000 --- a/settings/suggesters/FileSuggest.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AbstractInputSuggest, TFile } from 'obsidian'; - -export class FileSuggest extends AbstractInputSuggest { - protected getSuggestions(query: string): TFile[] | Promise { - const lowerCaseInputStr = query.toLowerCase(); - - // we do two filters because otherwise TS type inference does convert the array to TFile[] - return this.app.vault - .getAllLoadedFiles() - .filter(file => file instanceof TFile) - .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); - } - - renderSuggestion(value: TFile, el: HTMLElement): void { - el.setText(value.path); - } -} diff --git a/settings/suggesters/FolderSuggest.ts b/settings/suggesters/FolderSuggest.ts deleted file mode 100644 index eaee7153..00000000 --- a/settings/suggesters/FolderSuggest.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AbstractInputSuggest, TFolder } from 'obsidian'; - -export class FolderSuggest extends AbstractInputSuggest { - protected getSuggestions(query: string): TFolder[] | Promise { - const lowerCaseInputStr = query.toLowerCase(); - - // we do two filters because otherwise TS type inference does convert the array to TFolder[] - return this.app.vault - .getAllLoadedFiles() - .filter(file => file instanceof TFolder) - .filter(file => file.path.toLowerCase().contains(lowerCaseInputStr)); - } - - renderSuggestion(value: TFolder, el: HTMLElement): void { - el.setText(value.path); - } -} From 53a99c185ad6863283d6fdcc0c344fc7dff46fa0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:34:49 +0300 Subject: [PATCH 39/60] Delete api directory --- api/APIManager.ts | 113 - api/APIModel.ts | 46 - api/GeniusClient.ts | 86 - api/SpotifyClient.ts | 145 - api/apis/BoardGameGeekAPI.ts | 151 - api/apis/ComicVineAPI.ts | 121 - api/apis/GiantBombAPI.ts | 168 - api/apis/IGDBAPI.ts | 137 - api/apis/MALAPI.ts | 229 - api/apis/MALAPIManga.ts | 161 - api/apis/MobyGamesAPI.ts | 122 - api/apis/MusicBrainzAPI.ts | 328 - api/apis/MusicBrainzArtistAPI.ts | 249 - api/apis/OMDbAPI.ts | 291 - api/apis/OpenLibraryAPI.ts | 161 - api/apis/RAWGAPI.ts | 68 - api/apis/SteamAPI.ts | 245 - api/apis/TMDBMovieAPI.ts | 202 - api/apis/TMDBSeasonAPI.ts | 277 - api/apis/TMDBSeriesAPI.ts | 168 - api/apis/VNDBAPI.ts | 267 - api/apis/WikipediaAPI.ts | 112 - api/geniusLyricsExtract.ts | 87 - api/musicBrainzConstants.ts | 19 - api/schemas/GiantBomb.json | 11226 -------------- api/schemas/GiantBomb.ts | 6454 -------- api/schemas/MALAPI.ts | 6761 --------- api/schemas/OpenLibrary.json | 602 - api/schemas/OpenLibrary.ts | 578 - api/schemas/TMDB.ts | 22832 ----------------------------- 30 files changed, 52406 deletions(-) delete mode 100644 api/APIManager.ts delete mode 100644 api/APIModel.ts delete mode 100644 api/GeniusClient.ts delete mode 100644 api/SpotifyClient.ts delete mode 100644 api/apis/BoardGameGeekAPI.ts delete mode 100644 api/apis/ComicVineAPI.ts delete mode 100644 api/apis/GiantBombAPI.ts delete mode 100644 api/apis/IGDBAPI.ts delete mode 100644 api/apis/MALAPI.ts delete mode 100644 api/apis/MALAPIManga.ts delete mode 100644 api/apis/MobyGamesAPI.ts delete mode 100644 api/apis/MusicBrainzAPI.ts delete mode 100644 api/apis/MusicBrainzArtistAPI.ts delete mode 100644 api/apis/OMDbAPI.ts delete mode 100644 api/apis/OpenLibraryAPI.ts delete mode 100644 api/apis/RAWGAPI.ts delete mode 100644 api/apis/SteamAPI.ts delete mode 100644 api/apis/TMDBMovieAPI.ts delete mode 100644 api/apis/TMDBSeasonAPI.ts delete mode 100644 api/apis/TMDBSeriesAPI.ts delete mode 100644 api/apis/VNDBAPI.ts delete mode 100644 api/apis/WikipediaAPI.ts delete mode 100644 api/geniusLyricsExtract.ts delete mode 100644 api/musicBrainzConstants.ts delete mode 100644 api/schemas/GiantBomb.json delete mode 100644 api/schemas/GiantBomb.ts delete mode 100644 api/schemas/MALAPI.ts delete mode 100644 api/schemas/OpenLibrary.json delete mode 100644 api/schemas/OpenLibrary.ts delete mode 100644 api/schemas/TMDB.ts diff --git a/api/APIManager.ts b/api/APIManager.ts deleted file mode 100644 index bcdd5401..00000000 --- a/api/APIManager.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Notice } from 'obsidian'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from '../utils/MediaType'; -import { - isMusicBrainzFamilyDataSource, - musicBrainzRegisteredApiName, - MUSICBRAINZ_NOTE_DATA_SOURCE, -} from './musicBrainzConstants'; -import type { APIModel } from './APIModel'; - -export class APIManager { - apis: APIModel[]; - - constructor() { - this.apis = []; - } - - /** - * Queries the basic info for one query string and multiple APIs. - * - * @param query - * @param apisToQuery - */ - async query(query: string, apisToQuery: string[]): Promise { - console.debug(`MDB | api manager queried with "${query}"`); - - const promises = this.apis - .filter(api => apisToQuery.contains(api.apiName)) - .map(async api => { - try { - return await api.searchByTitle(query); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return []; - } - }); - - return (await Promise.all(promises)).flat(); - } - - /** - * Queries detailed information for a MediaTypeModel. - * - * @param item - */ - async queryDetailedInfo(item: MediaTypeModel): Promise { - 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 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, mediaType?: MediaType): Promise { - const trimmed = apiName.trim(); - const effectiveApiName = - trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) - ? MUSICBRAINZ_NOTE_DATA_SOURCE - : trimmed || apiName; - - if (isMusicBrainzFamilyDataSource(effectiveApiName) && mediaType !== undefined) { - const registeredName = musicBrainzRegisteredApiName(mediaType); - if (registeredName) { - const api = this.getApiByName(registeredName); - if (api) { - try { - return await api.getById(id); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return undefined; - } - } - } - } - - for (const api of this.apis) { - if (api.apiName === effectiveApiName) { - try { - return api.getById(id); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return undefined; - } - } - } - - return undefined; - } - - getApiByName(name: string): APIModel | undefined { - for (const api of this.apis) { - if (api.apiName === name) { - return api; - } - } - - return undefined; - } - - registerAPI(api: APIModel): void { - this.apis.push(api); - } -} diff --git a/api/APIModel.ts b/api/APIModel.ts deleted file mode 100644 index 50e6d72c..00000000 --- a/api/APIModel.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type MediaDbPlugin from '../main'; -import type { MediaTypeModel } from '../models/MediaTypeModel'; -import type { MediaType } from '../utils/MediaType'; - -export abstract class APIModel { - apiName!: string; - apiUrl!: string; - apiDescription!: string; - types!: MediaType[]; - plugin!: MediaDbPlugin; - - /** - * This function should query the api and return a list of matches. The matches should be capped at 20. - * - * @param title the title to query for - */ - abstract searchByTitle(title: string): Promise; - - abstract getById(id: string): Promise; - - abstract getDisabledMediaTypes(): MediaType[]; - - hasType(type: MediaType): boolean { - const disabledMediaTypes = this.getDisabledMediaTypes(); - return this.types.includes(type) && !disabledMediaTypes.includes(type); - } - - hasTypeOverlap(types: MediaType[]): boolean { - return types.some(type => this.hasType(type)); - } - - /** - * Returns the wiki-link string for a given property value. - * Subclasses can override this to apply API-specific file name templates - * (e.g. using an Artist file name for artist links). - * - * @param _property the property key (e.g. 'artists', 'albumTitle') - * @param value the raw string value to wrap - * @param _obj the full metadata object (for context) - * @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/') - */ - wikilinkValueFor(_property: string, value: string, _obj: Record, folderPrefix: string): string { - const clean = value.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; - return `[[${folderPrefix}${clean}|${clean}]]`; - } -} diff --git a/api/GeniusClient.ts b/api/GeniusClient.ts deleted file mode 100644 index 4b228177..00000000 --- a/api/GeniusClient.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { requestUrl } from 'obsidian'; - -import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; - -import { extractLyricsFromGeniusHtml } from './geniusLyricsExtract'; - -interface GeniusSearchHit { - result: { - id: number; - title: string; - url: string; - primary_artist: { name: string }; - }; -} - -interface GeniusSearchResponse { - response: { - hits: GeniusSearchHit[]; - }; -} - -export { extractLyricsFromGeniusHtml }; - -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 { - 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); - } -} diff --git a/api/SpotifyClient.ts b/api/SpotifyClient.ts deleted file mode 100644 index 48e87bae..00000000 --- a/api/SpotifyClient.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { requestUrl } from 'obsidian'; - -import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; - -interface SpotifyTokenResponse { - access_token: string; - expires_in: number; - token_type: string; -} - -interface SpotifySearchResponse { - tracks?: { - items: { external_urls?: { spotify?: string } }[]; - }; -} - -function spotifyTrackArtistQuery(trackTitle: string, artistName: string): string { - const clean = (s: string) => s.trim().replace(/"/g, ' ').replace(/\s+/g, ' '); - const t = clean(trackTitle); - const a = clean(artistName); - if (!t) { - return ''; - } - if (!a) { - return `track:"${t}"`; - } - return `track:"${t}" artist:"${a}"`; -} - -export class SpotifyClient { - private readonly clientId: string; - private readonly clientSecret: string; - private readonly userAgent: string; - private accessToken: string | null = null; - private tokenExpiresAtMs = 0; - - constructor(clientId: string | undefined, clientSecret: string | undefined) { - this.clientId = (clientId ?? '').trim(); - this.clientSecret = (clientSecret ?? '').trim(); - this.userAgent = `${pluginName}/${mediaDbVersion} (${contactEmail})`; - } - - isConfigured(): boolean { - return Boolean(this.clientId && this.clientSecret); - } - - private async refreshAccessToken(): Promise { - if (!this.isConfigured()) { - return null; - } - const basic = btoa(`${this.clientId}:${this.clientSecret}`); - const res = await requestUrl({ - url: 'https://accounts.spotify.com/api/token', - method: 'POST', - throw: false, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${basic}`, - 'User-Agent': this.userAgent, - }, - body: 'grant_type=client_credentials', - }); - if (res.status !== 200) { - console.warn(`MDB | Spotify token request returned ${res.status}`); - this.accessToken = null; - this.tokenExpiresAtMs = 0; - return null; - } - const data = res.json as SpotifyTokenResponse; - if (!data.access_token) { - return null; - } - this.accessToken = data.access_token; - const ttlMs = (data.expires_in ?? 3600) * 1000; - this.tokenExpiresAtMs = Date.now() + ttlMs - 60_000; - return this.accessToken; - } - - private async getAccessToken(): Promise { - if (!this.isConfigured()) { - return null; - } - const now = Date.now(); - if (this.accessToken && now < this.tokenExpiresAtMs) { - return this.accessToken; - } - return this.refreshAccessToken(); - } - - /** - * Search for a track and return the first result's open.spotify.com URL, or ''. - */ - async searchFirstTrackUrl(trackTitle: string, artistName: string): Promise { - const q = spotifyTrackArtistQuery(trackTitle, artistName); - if (!q) { - return ''; - } - let token = await this.getAccessToken(); - if (!token) { - console.warn('MDB | Spotify search fetch skipped: could not obtain access token'); - return ''; - } - - const params = new URLSearchParams({ q, type: 'track', limit: '1' }); - const url = `https://api.spotify.com/v1/search?${params.toString()}`; - console.log(`MDB | Spotify search fetch: ${url}`); - let res = await requestUrl({ - url, - method: 'GET', - throw: false, - headers: { - Authorization: `Bearer ${token}`, - 'User-Agent': this.userAgent, - }, - }); - - if (res.status === 401) { - this.accessToken = null; - this.tokenExpiresAtMs = 0; - token = await this.refreshAccessToken(); - if (!token) { - return ''; - } - console.log(`MDB | Spotify search fetch (retry after 401): ${url}`); - res = await requestUrl({ - url, - method: 'GET', - throw: false, - headers: { - Authorization: `Bearer ${token}`, - 'User-Agent': this.userAgent, - }, - }); - } - - if (res.status !== 200) { - console.warn(`MDB | Spotify search returned ${res.status}`); - return ''; - } - - const data = res.json as SpotifySearchResponse; - const link = data.tracks?.items?.[0]?.external_urls?.spotify; - return typeof link === 'string' ? link : ''; - } -} diff --git a/api/apis/BoardGameGeekAPI.ts b/api/apis/BoardGameGeekAPI.ts deleted file mode 100644 index bbe251d5..00000000 --- a/api/apis/BoardGameGeekAPI.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { requestUrl } from 'obsidian'; -import { BoardGameModel } from 'src/models/BoardGameModel'; -import type MediaDbPlugin from '../../main'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { coerceYear } from '../../utils/Utils'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -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://boardgamegeek.com/xmlapi/'; - this.types = [MediaType.BoardGame]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); - if (!bggKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/search?search=${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - Authorization: `Bearer ${bggKey}`, - }, - }); - - 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 = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - - // console.debug(response); - - const ret: MediaTypeModel[] = []; - - for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { - const id = boardgame.attributes.getNamedItem('objectid')?.value; - const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')?.textContent ?? undefined; - const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; - - ret.push( - new BoardGameModel({ - dataSource: this.apiName, - id, - title, - englishTitle: title, - year: coerceYear(year), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const bggKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.boardgameGeek); - if (!bggKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/boardgame/${encodeURIComponent(id)}?stats=1`; - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - Authorization: `Bearer ${bggKey}`, - }, - }); - - 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 = fetchData.text; - const response = new window.DOMParser().parseFromString(data, 'text/xml'); - // console.debug(response); - - const boardgame = response.querySelector('boardgame'); - if (!boardgame) { - throw Error(`MDB | Received invalid data from ${this.apiName}.`); - } - - const title = boardgame.querySelector('name[primary=true]')?.textContent; - const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; - const image = boardgame.querySelector('image')?.textContent ?? undefined; - const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); - const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')) - .map(n => n.textContent) - .filter(n => n !== null); - const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); - const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); - const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); - const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; - const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')) - .map(n => n.textContent) - .filter(n => n !== null); - - return new BoardGameModel({ - title: title ?? undefined, - englishTitle: title ?? undefined, - year: year === '0' ? 0 : coerceYear(year), - dataSource: this.apiName, - url: `https://boardgamegeek.com/boardgame/${id}`, - id: id, - - genres: genres, - onlineRating: onlineRating, - complexityRating: complexityRating, - minPlayers: minPlayers, - maxPlayers: maxPlayers, - playtime: playtime, - publishers: publishers, - image: image, - - released: true, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.BoardgameGeekAPI_disabledMediaTypes; - } -} diff --git a/api/apis/ComicVineAPI.ts b/api/apis/ComicVineAPI.ts deleted file mode 100644 index 1bfa3daa..00000000 --- a/api/apis/ComicVineAPI.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - -import { requestUrl } from 'obsidian'; -import { ComicMangaModel } from 'src/models/ComicMangaModel'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { MediaType } from '../../utils/MediaType'; -import { coerceYear } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -export class ComicVineAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'ComicVineAPI'; - this.apiDescription = 'A free API for comic books.'; - this.apiUrl = 'https://comicvine.gamespot.com/api'; - this.types = [MediaType.ComicManga]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); - if (!apiKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/search/?api_key=${apiKey}&format=json&resources=volume&query=${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - // console.debug(fetchData); - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const ret: MediaTypeModel[] = []; - for (const result of data.results) { - ret.push( - new ComicMangaModel({ - title: result.name, - englishTitle: result.name, - year: coerceYear(result.start_year), - dataSource: this.apiName, - id: `4050-${result.id}`, - publishers: result.publisher?.name, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.comicVine); - if (!apiKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/volume/${encodeURIComponent(id)}/?api_key=${apiKey}&format=json`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - const result = data.results; - - const authors = result.people as - | { - name: string; - }[] - | undefined; - - return new ComicMangaModel({ - type: MediaType.ComicManga, - title: result.name, - englishTitle: result.name, - alternateTitles: result.aliases, - plot: result.deck, - year: coerceYear(result.start_year), - dataSource: this.apiName, - url: result.site_detail_url, - id: `4050-${result.id}`, - - authors: authors?.map(x => x.name), - chapters: result.count_of_issues, - image: result.image?.original_url, - - released: true, - publishers: result.publisher?.name, - publishedFrom: result.start_year, - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.ComicVineAPI_disabledMediaTypes; - } -} diff --git a/api/apis/GiantBombAPI.ts b/api/apis/GiantBombAPI.ts deleted file mode 100644 index 3aba1336..00000000 --- a/api/apis/GiantBombAPI.ts +++ /dev/null @@ -1,168 +0,0 @@ -import createClient from 'openapi-fetch'; -import { coerceYear, obsidianFetch } from 'src/utils/Utils'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/GiantBomb'; - -export class GiantBombAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'GiantBombAPI'; - this.apiDescription = 'A free API for games.'; - this.apiUrl = 'https://www.giantbomb.com/api'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); - if (!apiKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/games', { - params: { - query: { - api_key: apiKey, - filter: `name:${title}`, - format: 'json', - limit: 20, - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.results; - - const ret: MediaTypeModel[] = []; - for (const result of data ?? []) { - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; - - ret.push( - new GameModel({ - title: result.name, - englishTitle: result.name, - year: coerceYear(year), - dataSource: this.apiName, - id: result.guid?.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.giantBomb); - if (!apiKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://www.giantbomb.com/api/' }); - const response = await client.GET('/game/{guid}', { - params: { - path: { - guid: id, - }, - query: { - api_key: apiKey, - format: 'json', - }, - }, - fetch: obsidianFetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.results; - - if (!result) { - throw Error(`MDB | No results found for ID ${id} in ${this.apiName}.`); - } - - console.log(result); - - // sadly the only OpenAPI definition I could find doesn't have the right types - const year = result.original_release_date ? new Date(result.original_release_date).getFullYear() : undefined; - const developers = result.developers as - | { - name: string; - }[] - | undefined; - const publishers = result.publishers as - | { - name: string; - }[] - | undefined; - const genres = result.genres as - | { - name: string; - }[] - | undefined; - const image = result.image as - | { - small_url: string; - medium_url: string; - super_url: string; - } - | undefined; - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: coerceYear(year), - dataSource: this.apiName, - url: result.site_detail_url, - id: result.guid?.toString(), - developers: developers?.map(x => x.name), - publishers: publishers?.map(x => x.name), - genres: genres?.map(x => x.name), - onlineRating: 0, - image: image?.super_url, - - released: true, - releaseDate: result.original_release_date, - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.GiantBombAPI_disabledMediaTypes; - } -} diff --git a/api/apis/IGDBAPI.ts b/api/apis/IGDBAPI.ts deleted file mode 100644 index 51306143..00000000 --- a/api/apis/IGDBAPI.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { MediaType } from '../../utils/MediaType'; -import { coerceYear } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -interface IGDBCover { url: string; } -interface IGDBGenre { name: string; } -interface IGDBCompany { name: string; } -interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } -interface IGDBPlatform { name: string; } -interface IGDBGameMode { name: string; } -interface IGDBCollection { name: string; } -interface IGDBGame { - id: number; name: string; cover?: IGDBCover; first_release_date?: number; - summary?: string; total_rating?: number; url?: string; - genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; - platforms?: IGDBPlatform[]; game_modes?: IGDBGameMode[]; - collection?: IGDBCollection; collections?: IGDBCollection[]; franchises?: IGDBCollection[]; -} -interface TwitchAuthResponse { access_token: string; expires_in: number; } - -export class IGDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - private accessToken: string = ''; - private tokenExpiry: number = 0; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'IGDBAPI'; - this.apiDescription = 'A free API for games (Requires Twitch Client ID & Secret).'; - this.apiUrl = 'https://api.igdb.com/v4'; - this.types = [MediaType.Game]; - } - - private async getAuthToken(): Promise { - const currentTime = Date.now(); - if (this.accessToken && currentTime < this.tokenExpiry) return this.accessToken; - - const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); - const clientSecret = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientSecret); - if (!clientId || !clientSecret) { - throw Error(`MDB | Client ID or Client Secret for ${this.apiName} missing.`); - } - console.log(`MDB | Refreshing Twitch Auth Token for ${this.apiName}`); - const response = await requestUrl({ - url: `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, - method: 'POST', - }); - if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); - const data = response.json as TwitchAuthResponse; - this.accessToken = data.access_token; - this.tokenExpiry = currentTime + (data.expires_in * 1000) - 60000; - return this.accessToken; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - const token = await this.getAuthToken(); - const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); - const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - return data.map(result => { - const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; - return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, year: coerceYear(year), - dataSource: this.apiName, id: result.id.toString(), image: image - }); - }); - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - const token = await this.getAuthToken(); - const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); - const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher, platforms.name, game_modes.name, collection.name, collections.name, franchises.name; where id = ${id};`; - const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, - body: queryBody, - }); - if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - - const data = response.json as IGDBGame[]; - if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); - const result = data[0]; - - const developers: string[] = []; - const publishers: string[] = []; - result.involved_companies?.forEach(c => { - if (c.developer) developers.push(c.company.name); - if (c.publisher) publishers.push(c.company.name); - }); - const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; - const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; - - let combinedSeries: string[] = []; - // Öncelik 1: Franchise (Ana marka) - result.franchises?.forEach(f => { if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); }); - - // Öncelik 2: Franchise yoksa Collection (Seri) fallback'i - if (combinedSeries.length === 0) { - if (result.collection?.name) combinedSeries.push(result.collection.name); - result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); - } - - return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, - year: coerceYear( - result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0, - ), - dataSource: this.apiName, url: result.url, id: result.id.toString(), - summary: result.summary ?? '', series: combinedSeries, - gameModes: result.game_modes?.map(g => g.name) || [], platforms: result.platforms?.map(p => p.name) || [], - developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], - onlineRating: result.total_rating ? Math.round(result.total_rating * 10) / 10 : 0, image: image, - released: result.first_release_date ? (result.first_release_date * 1000) <= Date.now() : false, - releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', - userData: { played: false, personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; } -} \ No newline at end of file diff --git a/api/apis/MALAPI.ts b/api/apis/MALAPI.ts deleted file mode 100644 index fc0c6986..00000000 --- a/api/apis/MALAPI.ts +++ /dev/null @@ -1,229 +0,0 @@ -import createClient from 'openapi-fetch'; -import { coerceMovieDurationMinutes, coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { SeriesModel } from '../../models/SeriesModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/MALAPI'; - -export class MALAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MALAPI'; - this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; - this.apiUrl = 'https://jikan.moe/'; - this.types = [MediaType.Movie, MediaType.Series]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('special', 'special'); - this.typeMappings.set('tv', 'series'); - this.typeMappings.set('ova', 'ova'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime', { - params: { - query: { - q: title, - limit: 20, - sfw: this.plugin.settings.sfwFilter ? true : false, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.data; - - const ret: MediaTypeModel[] = []; - - for (const result of data ?? []) { - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); - const id = result.mal_id?.toString(); - - if (type === undefined) { - ret.push( - new MovieModel({ - subType: '', - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - if (type === 'movie' || type === 'special') { - ret.push( - new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } else if (type === 'series' || type === 'ova') { - ret.push( - new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - year, - dataSource: this.apiName, - id, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/anime/{id}/full', { - params: { - path: { - id: id as unknown as number, // This is fine - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.data; - - if (result === undefined) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); - } - - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = coerceYear(result.year ?? result.aired?.prop?.from?.year); - const new_id = result.mal_id?.toString(); - - if (type === undefined) { - return new MovieModel({ - subType: undefined, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: coerceMovieDurationMinutes(result.duration), - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - if (type === 'movie' || type === 'special') { - return new MovieModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - duration: coerceMovieDurationMinutes(result.duration), - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - premiere: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series' || type === 'ova') { - return new SeriesModel({ - subType: type, - title: result.title, - englishTitle: result.title_english ?? result.title, - japaneseTitle: result.title_japanese, - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - plot: result.synopsis, - genres: result.genres?.map(x => x.name).filter(isTruthy), - studio: result.studios?.map(x => x.name).filter(isTruthy), - episodes: result.episodes, - duration: result.duration, - onlineRating: result.score, - streamingServices: result.streaming?.map(x => x.name).filter(isTruthy), - image: result.images?.jpg?.image_url, - - released: true, - ageRating: result.rating, - airedFrom: this.plugin.dateFormatter.format(result.aired?.from, this.apiDateFormat), - airedTo: this.plugin.dateFormatter.format(result.aired?.to, this.apiDateFormat), - airing: result.airing, - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MALAPI_disabledMediaTypes; - } -} diff --git a/api/apis/MALAPIManga.ts b/api/apis/MALAPIManga.ts deleted file mode 100644 index 4293ca1a..00000000 --- a/api/apis/MALAPIManga.ts +++ /dev/null @@ -1,161 +0,0 @@ -import createClient from 'openapi-fetch'; -import { coerceYear, isTruthy, obsidianFetch } from 'src/utils/Utils'; -import type MediaDbPlugin from '../../main'; -import { ComicMangaModel } from '../../models/ComicMangaModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/MALAPI'; - -export class MALAPIManga extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MALAPI Manga'; - this.apiDescription = 'A free API for Manga. Some results may take a long time to load.'; - this.apiUrl = 'https://jikan.moe/'; - this.types = [MediaType.ComicManga]; - this.typeMappings = new Map(); - this.typeMappings.set('manga', 'manga'); - this.typeMappings.set('manhwa', 'manhwa'); - this.typeMappings.set('doujinshi', 'doujin'); - this.typeMappings.set('one-shot', 'oneshot'); - this.typeMappings.set('manhua', 'manhua'); - this.typeMappings.set('light novel', 'light-novel'); - this.typeMappings.set('novel', 'novel'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/manga', { - params: { - query: { - q: title, - limit: 20, - sfw: this.plugin.settings.sfwFilter ? true : false, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data?.data; - - const ret: MediaTypeModel[] = []; - - for (const result of data ?? []) { - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = coerceYear(result.published?.prop?.from?.year); - const id = result.mal_id?.toString(); - - ret.push( - new ComicMangaModel({ - subType: type, - title: result.title, - plot: result.synopsis ?? undefined, - englishTitle: result.title_english ?? result.title, - alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), - year: year, - dataSource: this.apiName, - url: result.url, - id: id, - - genres: result.genres?.map(x => x.name).filter(isTruthy), - authors: result.authors?.map(x => x.name).filter(isTruthy), - chapters: result.chapters, - volumes: result.volumes, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), - publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://api.jikan.moe/v4/' }); - - const response = await client.GET('/manga/{id}/full', { - params: { - path: { - id: id as unknown as number, // This is fine - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data?.data; - - if (!result) { - throw Error(`MDB | No data found for ID ${id} in ${this.apiName}.`); - } - - const resType = result.type?.toLowerCase(); - const type = resType ? this.typeMappings.get(resType) : undefined; - const year = coerceYear(result.published?.prop?.from?.year); - const new_id = result.mal_id?.toString(); - - return new ComicMangaModel({ - subType: type, - title: result.title, - plot: result.synopsis ?? undefined, - englishTitle: result.title_english ?? result.title, - alternateTitles: result.titles?.map(x => x.title).filter(isTruthy), - year: year, - dataSource: this.apiName, - url: result.url, - id: new_id, - - genres: result.genres?.map(x => x.name).filter(isTruthy), - authors: result.authors?.map(x => x.name).filter(isTruthy), - chapters: result.chapters, - volumes: result.volumes, - onlineRating: result.score, - image: result.images?.jpg?.image_url, - - released: true, - publishedFrom: this.plugin.dateFormatter.format(result.published?.from, this.apiDateFormat), - publishedTo: this.plugin.dateFormatter.format(result.published?.to, this.apiDateFormat), - status: result.status, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MALAPIManga_disabledMediaTypes; - } -} diff --git a/api/apis/MobyGamesAPI.ts b/api/apis/MobyGamesAPI.ts deleted file mode 100644 index b201ad03..00000000 --- a/api/apis/MobyGamesAPI.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ - -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -// TODO: maybe we should remove this API, as it can no longer be tested without paying for an API key - -export class MobyGamesAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-DD-MM'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MobyGamesAPI'; - this.apiDescription = 'A free API for games.'; - this.apiUrl = 'https://api.mobygames.com/v1'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); - if (!apiKey) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?title=${encodeURIComponent(title)}&api_key=${apiKey}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - // console.debug(fetchData); - - if (fetchData.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (fetchData.status === 429) { - throw Error(`MDB | Too many requests for ${this.apiName}, you've exceeded your API quota.`); - } - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const ret: MediaTypeModel[] = []; - for (const result of data.games) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear(), - dataSource: this.apiName, - id: result.game_id, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.mobyGames); - if (!apiKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const searchUrl = `${this.apiUrl}/games?id=${encodeURIComponent(id)}&api_key=${apiKey}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = await fetchData.json; - // console.debug(data); - const result = data.games[0]; - - return new GameModel({ - type: MediaType.Game, - title: result.title, - englishTitle: result.title, - year: new Date(result.platforms[0].first_release_date).getFullYear(), - dataSource: this.apiName, - url: `https://www.mobygames.com/game/${result.game_id}`, - id: result.game_id, - developers: [], - publishers: [], - genres: result.genres?.map((x: any) => x.genre_name) ?? [], - onlineRating: result.moby_score, - image: result.sample_cover?.image ?? '', - - released: true, - releaseDate: result.platforms[0].first_release_date ?? 'unknown', - - userData: { - played: false, - - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MobyGamesAPI_disabledMediaTypes; - } -} diff --git a/api/apis/MusicBrainzAPI.ts b/api/apis/MusicBrainzAPI.ts deleted file mode 100644 index df6d0d16..00000000 --- a/api/apis/MusicBrainzAPI.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MusicReleaseModel } from '../../models/MusicReleaseModel'; -import { MediaType } from '../../utils/MediaType'; -import { contactEmail, coerceYear, getLanguageName, mediaDbVersion, pluginName } from '../../utils/Utils'; -import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; -import { APIModel } from '../APIModel'; - -// sadly no open api schema available - -interface Tag { - name: string; - count: number; -} -interface Genre { - name: string; - count: number; - id: string; - disambiguation: string; -} -interface Release { - id: string; - 'status-id': string; - title: string; - status: string; -} - -function pickNonBootlegRelease(releases: Release[] | undefined): Release | undefined { - return releases?.find(r => r.status !== 'Bootlet'); -} - -interface ArtistCredit { - name: string; - artist: { - tags: Tag[]; - type: string; - id: string; - name: string; - 'short-name': string; - country: string; - }; -} - -interface SearchResponse { - id: string; - 'type-id': string; - score: number; - 'primary-type-id': string; - 'artists-credit-id': string; - count: number; - title: string; - 'first-release-date': string; - 'primary-type': string; - 'artist-credit': ArtistCredit[]; - releases: Release[]; - tags: Tag[]; -} - -interface IdResponse { - id: string; - tags: Tag[]; - 'primary-type-id': string; - 'artist-credit': ArtistCredit[]; - title: string; - genres: Genre[]; - 'first-release-date': string; - releases: Release[]; - 'primary-type': string; - rating: { - value: number; - 'votes-count': number; - }; -} - -interface MediaResponse { - media: { - 'track-count': number; - tracks: { - 'artist-credit': ArtistCredit[]; - length: number | null; - number: string; - position: number; - title: string; - recording: { - id?: string; - length: number; - title: string; - }; - }[]; - }[]; - 'text-representation': { - language: string; - script: string; - }; -} - -export class MusicBrainzAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MusicBrainz API'; - this.apiDescription = 'Free API for music albums.'; - this.apiUrl = 'https://musicbrainz.org/'; - this.types = [MediaType.MusicRelease]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://musicbrainz.org/ws/2/release-group?query=${encodeURIComponent(title)}&limit=20&fmt=json`; - - const fetchData = await requestUrl({ - url: searchUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json) as { - 'release-groups': SearchResponse[]; - }; - // console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data['release-groups']) { - ret.push( - new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: coerceYear( - result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, - ), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - subType: result['primary-type'], - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - // Fetch release group - const groupUrl = `https://musicbrainz.org/ws/2/release-group/${encodeURIComponent(id)}?inc=releases+artists+tags+ratings+genres&fmt=json`; - const groupResponse = await requestUrl({ - url: groupUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (groupResponse.status !== 200) { - throw Error(`MDB | Received status code ${groupResponse.status} from ${this.apiName}.`); - } - - const result = (await groupResponse.json) as IdResponse; - - const firstRelease = pickNonBootlegRelease(result.releases); - if (!firstRelease) { - throw Error('MDB | No non-bootleg release found in release group.'); - } - - // Fetch recordings for the chosen release (skip MusicBrainz status=Bootleg when another edition exists) - const releaseUrl = `https://musicbrainz.org/ws/2/release/${firstRelease.id}?inc=recordings+artists&fmt=json`; - console.log(`MDB | Fetching release recordings from: ${releaseUrl}`); - - const releaseResponse = await requestUrl({ - url: releaseUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - - if (releaseResponse.status !== 200) { - throw Error(`MDB | Received status code ${releaseResponse.status} from ${this.apiName}.`); - } - - const releaseData = (await releaseResponse.json) as MediaResponse; - const tracks = extractTracksFromMedia(releaseData.media); - - // Calculate total album length for the first release - const totalrawLength = - releaseData.media[0]?.tracks.reduce((sum, track) => { - const len = track.length ?? track.recording?.length; - return typeof len === 'number' && !isNaN(len) ? sum + len : sum; - }, 0) ?? 0; - const albumLengthCalc = millisecondsToMinutes(totalrawLength); - - console.log(releaseData); - - return new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: coerceYear( - result['first-release-date'] ? new Date(result['first-release-date']).getFullYear() : 0, - ), - releaseDate: this.plugin.dateFormatter.format(result['first-release-date'], this.apiDateFormat) ?? 'unknown', - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: 'https://musicbrainz.org/release-group/' + result.id, - id: result.id, - image: 'https://coverartarchive.org/release-group/' + result.id + '/front-500.jpg', - - artists: result['artist-credit'].map(a => a.name), - language: releaseData['text-representation'].language ? getLanguageName(releaseData['text-representation'].language) : 'Unknown', - genres: result.genres.map(g => g.name), - subType: result['primary-type'], - albumDuration: albumLengthCalc, - trackCount: releaseData.media[0]?.['track-count'] ?? 0, - tracks: tracks, - rating: result.rating.value * 2, - - userData: { - personalRating: 0, - }, - }); - } - /** - * For the 'albumTitle' property (used on Song notes), the link target is - * derived from the MusicRelease file name template rather than the raw title. - */ - override wikilinkValueFor(property: string, value: string, obj: Record, folderPrefix: string): string { - if (property === 'albumTitle') { - const title = value.trim(); - const artistsRaw = obj.artists; - const artists = Array.isArray(artistsRaw) - ? artistsRaw.filter((a): a is string => typeof a === 'string') - : []; - const releaseModel = new MusicReleaseModel({ - type: 'musicRelease', title, englishTitle: title, - year: coerceYear(obj.year), releaseDate: '', dataSource: '', - url: '', id: '', image: '', artists, genres: [], - subType: 'album', language: '', rating: 0, - }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); - return linkTarget === title ? `[[${linkTarget}]]` : `[[${linkTarget}|${title}]]`; - } - return super.wikilinkValueFor(property, value, obj, folderPrefix); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; - } - - /** - * Loads MusicBrainz recording URL relations and returns the open.spotify.com track URL if present. - * Callers should throttle requests (~1/s) per MusicBrainz etiquette. - */ - async fetchSpotifyUrlForRecording(recordingId: string): Promise { - if (!recordingId) { - return ''; - } - const recordingUrl = `https://musicbrainz.org/ws/2/recording/${encodeURIComponent(recordingId)}?inc=url-rels&fmt=json`; - const fetchData = await requestUrl({ - url: recordingUrl, - headers: { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }, - }); - if (fetchData.status !== 200) { - console.warn(`MDB | Recording ${recordingId} url-rels returned ${fetchData.status}`); - return ''; - } - const data = (await fetchData.json) as RecordingUrlRelsResponse; - for (const rel of data.relations ?? []) { - const resource = rel.url?.resource; - if (typeof resource === 'string' && resource.includes('open.spotify.com')) { - return resource; - } - } - return ''; - } -} - -interface RecordingUrlRelsResponse { - relations?: { type: string; url?: { resource: string } }[]; -} - -function extractTracksFromMedia(media: MediaResponse['media']): { - number: number; - title: string; - duration: string; - featuredArtists: string[]; -}[] { - if (!media || media.length === 0 || !media[0].tracks) return []; - - return media[0].tracks.map((track, index) => { - const title = track.title ?? track.recording?.title ?? 'Unknown Title'; - const rawLength = track.length ?? track.recording?.length; - const duration = rawLength ? millisecondsToMinutes(rawLength) : 'unknown'; - const featuredArtists = track['artist-credit']?.map(ac => ac.name) ?? []; - const recordingId = track.recording?.id; - - return { - number: index + 1, - title, - duration, - featuredArtists, - ...(recordingId ? { recordingId } : {}), - }; - }); -} - -function millisecondsToMinutes(milliseconds: number): string { - const minutes = Math.floor(milliseconds / 60000); - const seconds = Math.floor((milliseconds % 60000) / 1000); - return `${minutes}:${seconds.toString().padStart(2, '0')}`; -} diff --git a/api/apis/MusicBrainzArtistAPI.ts b/api/apis/MusicBrainzArtistAPI.ts deleted file mode 100644 index 397b6c81..00000000 --- a/api/apis/MusicBrainzArtistAPI.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { ArtistModel } from '../../models/ArtistModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { coerceYear, contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; -import { MUSICBRAINZ_NOTE_DATA_SOURCE } from '../musicBrainzConstants'; -import { APIModel } from '../APIModel'; - -interface ArtistTag { - name: string; - count: number; -} - -interface ArtistGenre { - name: string; -} - -interface ArtistSearchArtist { - id: string; - name: string; - 'life-span'?: { begin?: string; end?: string }; - country?: string; - disambiguation?: string; - isnis?: string[]; -} - -interface ArtistSearchResponse { - artists: ArtistSearchArtist[]; -} - -interface ArtistDetailResponse { - id: string; - name: string; - type?: string; - 'life-span'?: { begin?: string; end?: string }; - country?: string; - disambiguation?: string; - isnis?: string[]; - tags?: ArtistTag[]; - genres?: ArtistGenre[]; - relations?: { url?: { resource: string } | null; type: string }[]; -} - -interface ReleaseGroupListItem { - id: string; - title: string; - 'primary-type': string; - 'secondary-types'?: string[]; - 'first-release-date'?: string; -} - -interface ReleaseGroupBrowseResponse { - 'release-groups': ReleaseGroupListItem[]; -} - -function isniFromMusicBrainz(isnis: string[] | undefined): string { - if (!isnis?.length) { - return ''; - } - return isnis.join(', '); -} - -const EXCLUDED_SECONDARY_TYPES = new Set([ - 'Compilation', - 'Live', - 'Remix', - 'Soundtrack', - 'Spokenword', - 'Interview', - 'Audio drama', - 'DJ-mix', - 'Mixtape/Street', - 'Demo', - 'Field recording', -]); - -export class MusicBrainzArtistAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'MusicBrainz Artist API'; - this.apiDescription = 'MusicBrainz artist search and studio album discography.'; - this.apiUrl = 'https://musicbrainz.org/'; - this.types = [MediaType.Artist]; - } - - private mbHeaders(): Record { - return { - 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, - }; - } - - private async throttleMs(ms: number): Promise { - await new Promise(resolve => setTimeout(resolve, ms)); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://musicbrainz.org/ws/2/artist?query=${encodeURIComponent(title)}&limit=20&fmt=json`; - const fetchData = await requestUrl({ - url: searchUrl, - headers: this.mbHeaders(), - }); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json) as ArtistSearchResponse; - const ret: MediaTypeModel[] = []; - - for (const artist of data.artists ?? []) { - const begin = artist['life-span']?.begin; - ret.push( - new ArtistModel({ - type: 'artist', - title: artist.name, - englishTitle: artist.name, - year: coerceYear(begin ? (begin.split('-')[0] ?? '') : ''), - beginYear: begin ? (begin.split('-')[0] ?? '') : '', - releaseDate: '', - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: 'https://musicbrainz.org/artist/' + artist.id, - id: artist.id, - country: artist.country ?? '', - disambiguation: artist.disambiguation ?? '', - isni: isniFromMusicBrainz(artist.isnis), - genres: [], - image: '', - officialWebsite: '', - subType: 'artist', - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const artistUrl = `https://musicbrainz.org/ws/2/artist/${encodeURIComponent(id)}?inc=tags+genres+url-rels&fmt=json`; - const res = await requestUrl({ - url: artistUrl, - headers: this.mbHeaders(), - }); - - if (res.status !== 200) { - throw Error(`MDB | Received status code ${res.status} from ${this.apiName}.`); - } - - const artist = (await res.json) as ArtistDetailResponse; - const begin = artist['life-span']?.begin; - const beginYear = begin ? (begin.split('-')[0] ?? '') : ''; - - let officialWebsite = ''; - for (const rel of artist.relations ?? []) { - if (rel.type === 'official homepage' && rel.url?.resource) { - officialWebsite = rel.url.resource; - break; - } - } - - return new ArtistModel({ - type: 'artist', - title: artist.name, - englishTitle: artist.name, - year: coerceYear(beginYear), - beginYear, - releaseDate: begin ? (this.plugin.dateFormatter.format(begin, this.apiDateFormat) ?? 'unknown') : '', - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: 'https://musicbrainz.org/artist/' + artist.id, - id: artist.id, - country: artist.country ?? '', - disambiguation: artist.disambiguation ?? '', - isni: isniFromMusicBrainz(artist.isnis), - genres: [...new Set([...(artist.genres?.map(g => g.name) ?? []), ...(artist.tags?.map(t => t.name) ?? [])])], - image: '', - officialWebsite, - subType: 'artist', - userData: { - personalRating: 0, - }, - }); - } - - /** - * Lists release group MBIDs for studio albums (primary type album, excluding live/compilations/etc.). - * Passes release-group-status=website-default so MusicBrainz omits groups that only have bootleg, promotional, or pseudo-releases - * (see MusicBrainz API “Release (Group) Type and Status”). - */ - async listStudioAlbumReleaseGroupIds(artistId: string): Promise { - const collected: { id: string; date: string }[] = []; - let offset = 0; - const limit = 100; - - while (true) { - await this.throttleMs(1100); - const url = `https://musicbrainz.org/ws/2/release-group?artist=${encodeURIComponent(artistId)}&type=album&fmt=json&limit=${limit}&offset=${offset}&release-group-status=website-default`; - - const res = await requestUrl({ - url, - headers: this.mbHeaders(), - }); - - if (res.status !== 200) { - throw Error(`MDB | Received status code ${res.status} browsing release groups.`); - } - - const data = (await res.json) as ReleaseGroupBrowseResponse; - const groups = data['release-groups'] ?? []; - if (groups.length === 0) { - break; - } - - for (const rg of groups) { - if (rg['primary-type'] !== 'Album') { - continue; - } - const secondary = rg['secondary-types'] ?? []; - if (secondary.some(t => EXCLUDED_SECONDARY_TYPES.has(t))) { - continue; - } - collected.push({ - id: rg.id, - date: rg['first-release-date'] ?? '', - }); - } - - offset += limit; - if (groups.length < limit) { - break; - } - } - - collected.sort((a, b) => a.date.localeCompare(b.date)); - return [...new Set(collected.map(c => c.id))]; - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.MusicBrainzArtistAPI_disabledMediaTypes; - } -} diff --git a/api/apis/OMDbAPI.ts b/api/apis/OMDbAPI.ts deleted file mode 100644 index 28d69242..00000000 --- a/api/apis/OMDbAPI.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { SeriesModel } from '../../models/SeriesModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { MediaType } from '../../utils/MediaType'; -import { coerceMovieDurationMinutes, coerceYear } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -interface ErrorResponse { - Response: 'False'; - Error: string; -} - -type SearchResponse = - | { - Response: 'True'; - totalResults: string; - Search: { - Title: string; - Year: string; - Poster: string; - imdbID: string; - Type: string; - }[]; - } - | ErrorResponse; - -type IdResponse = - | { - Response: 'True'; - Title: string; - Year: string; - Rated: string; - Released: string; - Runtime: string; - Genre: string; - Director: string; - Writer: string; - Actors: string; - Plot: string; - Language: string; - Country: string; - Awards: string; - Poster: string; - Metascore: string; - imdbRating: string; - imdbVotes: string; - imdbID: string; - Type: string; - DVD: string; - BoxOffice: string; - Production: string; - Website: string; - } - | ErrorResponse; - -export class OMDbAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'DD MMM YYYY'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OMDbAPI'; - this.apiDescription = 'A free API for Movies, Series and Games.'; - this.apiUrl = 'https://www.omdbapi.com/'; - this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; - this.typeMappings = new Map(); - this.typeMappings.set('movie', 'movie'); - this.typeMappings.set('series', 'series'); - this.typeMappings.set('game', 'game'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); - if (!omdbKey) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?s=${encodeURIComponent(title)}&apikey=${omdbKey}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const data = response.json as SearchResponse | undefined; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.Response === 'False') { - if (data.Error === 'Movie not found!') { - return []; - } - - throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`); - } - if (!data.Search) { - return []; - } - - // console.debug(data.Search); - - const ret: MediaTypeModel[] = []; - - for (const result of data.Search) { - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - continue; - } - if (type === 'movie') { - ret.push( - new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'series') { - ret.push( - new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } else if (type === 'game') { - ret.push( - new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - id: result.imdbID, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const omdbKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.omdb); - if (!omdbKey) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const response = await requestUrl({ - url: `https://www.omdbapi.com/?i=${encodeURIComponent(id)}&apikey=${omdbKey}`, - method: 'GET', - }); - - if (response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.status !== 200) { - throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - } - - const result = response.json as IdResponse | undefined; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (result.Response === 'False') { - throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); - } - - const type = this.typeMappings.get(result.Type.toLowerCase()); - if (type === undefined) { - throw Error(`${result.Type.toLowerCase()} is an unsupported type.`); - } - - if (type === 'movie') { - return new MovieModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - director: result.Director?.split(', '), - writer: result.Writer?.split(', '), - duration: coerceMovieDurationMinutes(result.Runtime), - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - revenue: result.BoxOffice && result.BoxOffice !== 'N/A' ? result.BoxOffice : '', - ageRating: result.Rated, - premiere: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'series') { - return new SeriesModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - plot: result.Plot, - genres: result.Genre?.split(', '), - writer: result.Writer?.split(', '), - studio: [], - episodes: 0, - duration: result.Runtime, - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - actors: result.Actors?.split(', '), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - country: result.Country?.split(', '), - ageRating: result.Rated, - airedFrom: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } else if (type === 'game') { - return new GameModel({ - type: type, - title: result.Title, - englishTitle: result.Title, - year: coerceYear(result.Year), - dataSource: this.apiName, - url: `https://www.imdb.com/title/${result.imdbID}/`, - id: result.imdbID, - - genres: result.Genre?.split(', '), - onlineRating: Number.parseFloat(result.imdbRating ?? 0), - image: result.Poster.replace('_SX300', '_SX600'), - - released: true, - releaseDate: this.plugin.dateFormatter.format(result.Released, this.apiDateFormat), - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - throw new Error(`MDB | Unknown media type for id ${id}`); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OMDbAPI_disabledMediaTypes; - } -} diff --git a/api/apis/OpenLibraryAPI.ts b/api/apis/OpenLibraryAPI.ts deleted file mode 100644 index ae9b74ed..00000000 --- a/api/apis/OpenLibraryAPI.ts +++ /dev/null @@ -1,161 +0,0 @@ -import createClient from 'openapi-fetch'; -import { BookModel } from 'src/models/BookModel'; -import { obsidianFetch } from 'src/utils/Utils'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/OpenLibrary'; - -interface SearchResponse { - editions: { - docs: { - key?: string; - title?: string; - cover_i?: number; - isbn?: string[]; - }[]; - }; - cover_i?: number; - has_fulltext?: boolean; - edition_count?: number; - title?: string; - author_name?: string[]; - first_publish_year?: number; - key: string; - description?: string; - - number_of_pages_median?: number; - isbn?: string[]; - ratings_average?: number; -} - -export class OpenLibraryAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'OpenLibraryAPI'; - this.apiDescription = 'A free API for books'; - this.apiUrl = 'https://openlibrary.org/'; - this.types = [MediaType.Book]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: title, - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - }; - - // console.debug(data); - - const ret: MediaTypeModel[] = []; - - for (const result of data.docs) { - ret.push( - new BookModel({ - title: result.title, - englishTitle: result.title, - year: result.first_publish_year ?? 0, - dataSource: this.apiName, - id: result.key, - author: result.author_name?.join(', '), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - - const response = await client.GET('/search.json', { - params: { - query: { - q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', - }, - }, - fetch: obsidianFetch, - }); - - if (response.error !== undefined) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data as { - docs: SearchResponse[]; - q?: string; - }; - - const result = data.docs[0]; - - let key = result.key; - let title = result.title; - let cover_i = result.cover_i; - let isbnArr = result.isbn; - - // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present - const q = data.q ?? ''; - if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { - const edition = result.editions.docs[0]; - key = edition.key ?? key; - title = edition.title ?? title; - cover_i = edition.cover_i ?? cover_i; - isbnArr = edition.isbn ?? isbnArr; - } - - const pages = Number(result.number_of_pages_median); - const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); - const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); - - return new BookModel({ - title: title, - year: result.first_publish_year ?? 0, - dataSource: this.apiName, - url: `https://openlibrary.org` + key, - id: key, - isbn: Number.isNaN(isbn) ? undefined : isbn, - isbn13: Number.isNaN(isbn13) ? undefined : isbn13, - englishTitle: title, - - author: result.author_name?.join(', '), - plot: result.description ?? undefined, - pages: Number.isNaN(pages) ? undefined : pages, - onlineRating: result.ratings_average, - image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, - - released: true, - - userData: { - read: false, - lastRead: '', - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; - } -} diff --git a/api/apis/RAWGAPI.ts b/api/apis/RAWGAPI.ts deleted file mode 100644 index c22c9a9a..00000000 --- a/api/apis/RAWGAPI.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface RAWGGame { - id: number; name: string; released?: string; background_image?: string; - name_original?: string; website?: string; slug?: string; metacritic?: number; - developers?: { name: string }[]; publishers?: { name: string }[]; genres?: { name: string }[]; -} -interface RAWGSearchResponse { results: RAWGGame[]; } - -export class RAWGAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; - - constructor(plugin: MediaDbPlugin) { - super(); - this.plugin = plugin; - this.apiName = 'RAWGAPI'; - this.apiDescription = 'A large open video game database.'; - this.apiUrl = 'https://api.rawg.io/api'; - this.types = [MediaType.Game]; - } - - async searchByTitle(title: string): Promise { - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); - if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); - const response = await requestUrl({ - url: `${this.apiUrl}/games?key=${apiKey}&search=${encodeURIComponent(title)}&page_size=20`, - method: 'GET', - }); - if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - - const data = response.json as RAWGSearchResponse; - return data.results.map(result => new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear() : 0, - dataSource: this.apiName, id: result.id.toString(), image: result.background_image - })); - } - - async getById(id: string): Promise { - const apiKey = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.rawg); - if (!apiKey) throw Error(`MDB | API key for ${this.apiName} missing.`); - const response = await requestUrl({ - url: `${this.apiUrl}/games/${id}?key=${apiKey}`, - method: 'GET', - }); - if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); - - const result = response.json as RAWGGame; - return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, - year: result.released ? new Date(result.released).getFullYear() : 0, - dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], - publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], - onlineRating: result.metacritic, image: result.background_image, - released: result.released != null, releaseDate: result.released, - userData: { played: false, personalRating: 0 }, - }); - } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; } -} \ No newline at end of file diff --git a/api/apis/SteamAPI.ts b/api/apis/SteamAPI.ts deleted file mode 100644 index 8d8f78a0..00000000 --- a/api/apis/SteamAPI.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { coerceYear, imageUrlExists } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -interface SearchResponse { - appid: string; - name: string; - icon: string; - logo: string; -} - -type IdResponse = Record< - string, - { - success: boolean; - data: GameDetails; - } ->; - -interface GameDetails { - type: string; - name: string; - steam_appid: number; - required_age: string; - is_free: boolean; - controller_support: string; - dlc: number[]; - detailed_description: string; - about_the_game: string; - short_description: string; - supported_languages: string; - reviews: string; - header_image: string; - capsule_image: string; - capsule_imagev5: string; - website: string; - pc_requirements: Requirements; - mac_requirements: Requirements; - linux_requirements: Requirements; - legal_notice: string; - drm_notice: string; - developers: string[]; - publishers: string[]; - price_overview: PriceOverview; - packages: number[]; - platforms: Platforms; - metacritic?: { - score: number; - url: string; - }; - categories: Category[]; - genres: Genre[]; - recommendations: { - total: number; - }; - achievements: { - total: number; - highlighted: Achievement[]; - }; - release_date: { - coming_soon: boolean; - date: string; - }; - support_info: { - url: string; - email: string; - }; - background: string; - background_raw: string; - content_descriptors: { - ids: number[]; - notes: string; - }; - ratings: Ratings; -} - -interface Requirements { - minimum: string; - recommended: string; -} - -interface PriceOverview { - currency: string; - initial: number; - final: number; - discount_percent: number; - initial_formatted: string; - final_formatted: string; -} - -interface Platforms { - windows: boolean; - mac: boolean; - linux: boolean; -} - -interface Category { - id: number; - description: string; -} - -interface Genre { - id: string; - description: string; -} - -interface Achievement { - name: string; - path: string; -} - -type Ratings = Record< - string, - { - rating: string; - descriptors: string; - use_age_gate: string; - required_age: string; - rating_id?: string; - banned?: string; - rating_generated?: string; - } ->; - -export class SteamAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - apiDateFormat: string = 'DD MMM, YYYY'; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'SteamAPI'; - this.apiDescription = 'A free API for all Steam games.'; - this.apiUrl = 'https://www.steampowered.com/'; - this.types = [MediaType.Game]; - this.typeMappings = new Map(); - this.typeMappings.set('game', 'game'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://steamcommunity.com/actions/SearchApps/${encodeURIComponent(title)}`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json) as SearchResponse[]; - - // console.debug(data); - - const ret: MediaTypeModel[] = []; - - for (const result of data) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: 0, - dataSource: this.apiName, - id: result.appid, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const searchUrl = `https://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; - const fetchData = await requestUrl({ - url: searchUrl, - }); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - // console.debug(await fetchData.json); - const data = (await fetchData.json) as IdResponse; - - let result: GameDetails | undefined = undefined; - for (const [key, value] of Object.entries(data)) { - // after some testing I found out that id is somehow a number despite that it's defined as string... - if (key === String(id)) { - result = value.data; - } - } - if (!result) { - throw Error(`MDB | API returned invalid data.`); - } - - // console.debug(result); - - // Check if a poster version of the image exists, else use the header image - const imageUrl = `https://steamcdn-a.akamaihd.net/steam/apps/${result.steam_appid}/library_600x900_2x.jpg`; - const exists = await imageUrlExists(imageUrl); - let finalimageurl; - if (exists) { - finalimageurl = imageUrl; - } else { - finalimageurl = result.header_image ?? ''; - } - - return new GameModel({ - type: MediaType.Game, - title: result.name, - englishTitle: result.name, - year: coerceYear(new Date(result.release_date.date).getFullYear()), - dataSource: this.apiName, - url: `https://store.steampowered.com/app/${result.steam_appid}`, - id: result.steam_appid.toString(), - - developers: result.developers, - publishers: result.publishers, - genres: result.genres?.map(x => x.description), - onlineRating: result.metacritic?.score, - image: finalimageurl, - - released: !result.release_date?.coming_soon, - releaseDate: this.plugin.dateFormatter.format(result.release_date?.date, this.apiDateFormat), - - userData: { - played: false, - personalRating: 0, - }, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.SteamAPI_disabledMediaTypes; - } -} diff --git a/api/apis/TMDBMovieAPI.ts b/api/apis/TMDBMovieAPI.ts deleted file mode 100644 index ea0cd19a..00000000 --- a/api/apis/TMDBMovieAPI.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ - -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MovieModel } from '../../models/MovieModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { MediaType } from '../../utils/MediaType'; -import { formatUsdWholeDollars } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -/** TMDB `credits.crew` jobs that count as writing credits for movies. */ -const TMDB_WRITING_CREW_JOBS = new Set([ - 'Writer', - 'Screenplay', - 'Story', - 'Teleplay', - 'Original Story', - 'Characters', - 'Novel', - 'Screenstory', -]); - -function tmdbWritingCreditsFromCrew(crew: any[] | undefined): string[] { - if (!crew?.length) { - return []; - } - const seen = new Set(); - const names: string[] = []; - for (const c of crew) { - const job = c?.job as string | undefined; - const name = c?.name as string | undefined; - if (!job || !name || !TMDB_WRITING_CREW_JOBS.has(job)) { - continue; - } - if (!seen.has(name)) { - seen.add(name); - names.push(name); - } - } - return names; -} - -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`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/search/movie', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !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() : 0, - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/movie/{movie_id}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { movie_id: parseInt(id) }, - query: { - append_to_response: 'credits,release_dates,watch/providers', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // console.debug(result); - - return new MovieModel({ - type: 'movie', - title: result.title, - englishTitle: result.title, - year: result.release_date ? new Date(result.release_date).getFullYear() : 0, - 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.toString(), - - plot: result.overview ?? '', - genres: result.genres?.map((g: any) => g.name) ?? [], - // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type - writer: tmdbWritingCreditsFromCrew((result as { credits?: { crew?: any[] } }).credits?.crew), - // @ts-ignore - 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 != null && Number.isFinite(result.runtime) ? Math.trunc(result.runtime) : 0, - onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, - // @ts-ignore - 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!), - country: result.production_countries?.map((c: any) => c.name) ?? [], - language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], - budget: formatUsdWholeDollars(result.budget ?? 0), - revenue: formatUsdWholeDollars(result.revenue ?? 0), - // @ts-ignore - ageRating: result.release_dates?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.release_dates?.[0]?.certification ?? '', - // @ts-ignore - streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], - - userData: { - watched: false, - lastWatched: '', - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.TMDBMovieAPI_disabledMediaTypes; - } -} diff --git a/api/apis/TMDBSeasonAPI.ts b/api/apis/TMDBSeasonAPI.ts deleted file mode 100644 index b91b50a0..00000000 --- a/api/apis/TMDBSeasonAPI.ts +++ /dev/null @@ -1,277 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ - -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { SeasonModel } from '../../models/SeasonModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -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`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const searchResponse = await client.GET('/3/search/tv', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (searchResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (searchResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${searchResponse.response.status} from ${this.apiName}.`); - } - - const searchData = searchResponse.data; - - if (!searchData?.results || searchData.total_results === 0) { - return []; - } - - const ret: MediaTypeModel[] = []; - - for (const result of searchData.results) { - if (ret.length >= 20) break; - - // Fetch series details to get the total number of seasons - let totalSeasons = 0; - try { - const detailsResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { series_id: result.id ?? 0 }, - }, - fetch: fetch, - }); - - if (detailsResponse.response.status === 200 && detailsResponse.data) { - const detailsData = detailsResponse.data; - if (Array.isArray(detailsData.seasons)) { - totalSeasons = detailsData.seasons.length; - } - } - } catch { - // Ignore errors and assume 0 seasons - } - - ret.push( - new SeasonModel({ - title: `${result.name ?? result.original_name ?? ''}`, - englishTitle: result.name ?? result.original_name ?? '', - year: result.first_air_date ? new Date(result.first_air_date).getFullYear() : 0, - dataSource: this.apiName, - id: result.id?.toString() ?? '', - seasonTitle: result.name ?? result.original_name ?? '', - seasonNumber: totalSeasons, - }), - ); - } - - return ret; - } - - // Fetch all seasons for a given series - async getSeasonsForSeries(tvId: string): Promise { - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const seriesResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { series_id: parseInt(tvId) }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - const seriesName = seriesData?.name ?? ''; - - const ret: SeasonModel[] = []; - - if (Array.isArray(seriesData?.seasons)) { - for (const season of seriesData.seasons) { - const seasonNumber = season.season_number ?? 0; - const titleText = `${seriesName} - Season ${seasonNumber}`; - - ret.push( - new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: season.air_date ? new Date(season.air_date).getFullYear() : 0, - dataSource: this.apiName, - id: `${tvId}/season/${seasonNumber}`, - seasonTitle: season.name ?? titleText, - seasonNumber: seasonNumber, - }), - ); - } - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - // Expect season ids like "12345/season/2" - const m = /^(\d+)\/season\/(\d+)$/.exec(id); - if (!m) { - throw Error(`MDB | Invalid season id "${id}". Expected format "/season/".`); - } - - const tvId = m[1]; - const seasonNumber = m[2]; - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - - // Fetch season details - const seasonResponse = await client.GET('/3/tv/{series_id}/season/{season_number}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { - series_id: parseInt(tvId), - season_number: parseInt(seasonNumber), - }, - }, - fetch: fetch, - }); - - if (seasonResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seasonResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seasonResponse.response.status} from ${this.apiName}.`); - } - - const seasonData = seasonResponse.data; - if (!seasonData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // Fetch parent series to build consistent titles and inherit fields - const seriesResponse = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { series_id: parseInt(tvId) }, - query: { - append_to_response: 'credits', - }, - }, - fetch: fetch, - }); - - if (seriesResponse.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - - if (seriesResponse.response.status !== 200) { - throw Error(`MDB | Received status code ${seriesResponse.response.status} from ${this.apiName}.`); - } - - const seriesData = seriesResponse.data; - - if (!seriesData) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - const seriesName = seriesData?.name ?? ''; - const airDate = seasonData.air_date ?? ''; - const titleText = `${seriesName} - Season ${seasonData.season_number}`; - - // Get airedTo as the air_date of the last episode, if available - let airedTo = 'unknown'; - if (Array.isArray(seasonData.episodes) && seasonData.episodes.length > 0) { - const lastEp = seasonData.episodes[seasonData.episodes.length - 1]; - if (lastEp?.air_date) airedTo = lastEp.air_date; - } - - return new SeasonModel({ - title: titleText, - englishTitle: titleText, - year: airDate ? new Date(airDate).getFullYear() : 0, - dataSource: this.apiName, - url: `https://www.themoviedb.org/tv/${tvId}/season/${seasonData.season_number}`, - id: `${tvId}/season/${seasonData.season_number}`, - seasonTitle: seasonData.name ?? titleText, - seasonNumber: seasonData.season_number ?? Number(seasonNumber), - episodes: Array.isArray(seasonData.episodes) ? seasonData.episodes.length : 0, - airedFrom: this.plugin.dateFormatter.format(airDate, this.apiDateFormat) ?? 'unknown', - airedTo: airedTo, - plot: seasonData.overview ?? '', - image: seasonData.poster_path ? `https://image.tmdb.org/t/p/w780${seasonData.poster_path}` : '', - genres: seriesData.genres?.map(g => g.name ?? '').filter(name => name !== '') ?? [], - writer: seriesData.created_by?.map(c => c.name ?? '').filter(name => name !== '') ?? [], - studio: seriesData.production_companies?.map(s => s.name ?? '').filter(name => name !== '') ?? [], - duration: seriesData.episode_run_time?.[0]?.toString() ?? '', - onlineRating: seasonData.vote_average ?? 0, - // @ts-ignore - append_to_response credits not reflected in base schema - actors: seriesData.credits?.cast?.map((c: any) => c.name).slice(0, 5) ?? [], - released: ['Returning Series', 'Cancelled', 'Ended'].includes(seriesData.status ?? ''), - streamingServices: [], - airing: ['Returning Series'].includes(seriesData.status ?? ''), - userData: { watched: false, lastWatched: '', personalRating: 0 }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return []; - } -} diff --git a/api/apis/TMDBSeriesAPI.ts b/api/apis/TMDBSeriesAPI.ts deleted file mode 100644 index 61fe68f3..00000000 --- a/api/apis/TMDBSeriesAPI.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ - -import createClient from 'openapi-fetch'; -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { SeriesModel } from '../../models/SeriesModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; -import type { paths } from '../schemas/TMDB'; - -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`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw new Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/search/tv', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - query: { - query: encodeURIComponent(title), - include_adult: this.plugin.settings.sfwFilter ? false : true, - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const data = response.data; - - if (!data) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - - if (data.total_results === 0 || !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() : 0, - dataSource: this.apiName, - id: result.id.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const bearer = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.tmdb); - if (!bearer) { - throw Error(`MDB | API key for ${this.apiName} missing.`); - } - - const client = createClient({ baseUrl: 'https://api.themoviedb.org' }); - const response = await client.GET('/3/tv/{series_id}', { - headers: { - Authorization: `Bearer ${bearer}`, - }, - params: { - path: { series_id: parseInt(id) }, - query: { - append_to_response: 'credits,content_ratings,watch/providers', - }, - }, - fetch: fetch, - }); - - if (response.response.status === 401) { - throw Error(`MDB | Authentication for ${this.apiName} failed. Check the API key.`); - } - if (response.response.status !== 200) { - throw Error(`MDB | Received status code ${response.response.status} from ${this.apiName}.`); - } - - const result = response.data; - - if (!result) { - throw Error(`MDB | No data received from ${this.apiName}.`); - } - // 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() : 0, - dataSource: this.apiName, - url: `https://www.themoviedb.org/tv/${result.id}`, - id: result.id.toString(), - - 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]?.toString() ?? 'unknown', - onlineRating: result.vote_average ? Math.round(result.vote_average * 10) / 10 : 0, - // TMDB's spec allows for 'append_to_response' but doesn't seem to account for it in the type - // @ts-ignore - actors: result.credits?.cast.map((c: any) => c.name).slice(0, 5) ?? [], - image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : null, - - released: ['Returning Series', 'Cancelled', 'Canceled', 'Pilot', 'Ended'].includes(result.status!), - country: result.production_countries?.map((c: any) => c.name) ?? [], - language: result.spoken_languages?.map((l: any) => l.english_name) ?? [], - network: result.networks?.map((n: any) => n.name) ?? [], - // @ts-ignore - ageRating: result.content_ratings?.results?.find((r: any) => r.iso_3166_1 === this.plugin.settings.tmdbRegion)?.rating ?? '', - // @ts-ignore - streamingServices: result['watch/providers']?.results?.[this.plugin.settings.tmdbRegion]?.flatrate?.map((p: any) => p.provider_name) ?? [], - 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; - } -} diff --git a/api/apis/VNDBAPI.ts b/api/apis/VNDBAPI.ts deleted file mode 100644 index 2a933dae..00000000 --- a/api/apis/VNDBAPI.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { requestUrl } from 'obsidian'; -import type MediaDbPlugin from '../../main'; -import { GameModel } from '../../models/GameModel'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { MediaType } from '../../utils/MediaType'; -import { coerceYear } from '../../utils/Utils'; -import { APIModel } from '../APIModel'; - -enum VNDevStatus { - Finished, - InDevelopment, - Cancelled, -} - -enum TagSpoiler { - None, - Minor, - Major, -} - -enum TagCategory { - Content = 'cont', - Sexual = 'ero', - Technical = 'tech', -} - -/** - * A partial `POST /vn` response payload; desired fields should be listed in the request body. - */ -interface VNJSONResponse { - more: boolean; - results: [ - { - id: string; - title: string; - titles: [ - { - title: string; - lang: string; - }, - ]; - devstatus: VNDevStatus; - released: string | 'TBA' | null; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents - image: { - url: string; - sexual: number; - } | null; - rating: number | null; - tags: [ - { - id: string; - name: string; - category: TagCategory; - rating: number; - spoiler: TagSpoiler; - }, - ]; - developers: [ - { - id: string; - name: string; - }, - ]; - }, - ]; -} - -/** - * A partial `POST /release` response payload; desired fields should be listed in the request body. - */ -interface ReleaseJSONResponse { - more: boolean; - results: [ - { - id: string; - producers: [ - { - id: string; - name: string; - developer: boolean; - publisher: boolean; - }, - ]; - }, - ]; -} - -export class VNDBAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DD'; // Can also return YYYY-MM or YYYY - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'VNDB API'; - this.apiDescription = 'A free API for visual novels.'; - this.apiUrl = 'https://api.vndb.org/kana'; - this.types = [MediaType.Game]; - } - - /** - * Make a `POST` request to the VNDB API. - * @param endpoint The API endpoint to query. E.g. "/vn". - * @param body A JSON object defining the query, following the VNDB API structure. - * @returns A JSON object representing the query response. - * @throws Error The request returned an unsuccessful or unexpected HTTP status code. - * @see {@link https://api.vndb.org/kana#api-structure} - */ - private async postQuery(endpoint: string, body: string): Promise { - const fetchData = await requestUrl({ - url: `${this.apiUrl}${endpoint}`, - method: 'POST', - contentType: 'application/json', - body: body, - throw: false, - }); - - if (fetchData.status !== 200) { - switch (fetchData.status) { - case 400: - throw Error(`MDB | Invalid request body or query [${fetchData.text}].`); - case 404: - throw Error(`MDB | Invalid API path or HTTP method.`); - case 429: - throw Error(`MDB | Throttled.`); - case 500: - throw Error(`MDB | VNDB server error.`); - case 502: - throw Error(`MDB | VNDB server is down.`); - default: - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - } - - return fetchData.json; - } - - /** - * Make a `POST` request to the `/vn` endpoint. - * Queries visual novel entries. - * @see {@link https://api.vndb.org/kana#post-vn} - */ - private postVNQuery(body: string): Promise { - return this.postQuery('/vn', body) as Promise; - } - - /** - * Make a `POST` request to the `/release` endpoint. - * Queries release entries. - * @see {@link https://api.vndb.org/kana#post-release} - */ - private postReleaseQuery(body: string): Promise { - return this.postQuery('/release', body) as Promise; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - /* SFW Filter: has ANY official&&complete&&standalone&&SFW release - OR has NO official&&standalone&&NSFW release - OR has the `In-game Sexual Content Toggle` (g2708) tag */ - // prettier-ignore - const vnData = await this.postVNQuery(`{ - "filters": ["and" ${!this.plugin.settings.sfwFilter ? `` : - `, ["or" - , ["release", "=", ["and" - , ["official", "=", "1"] - , ["rtype", "=", "complete"] - , ["patch", "!=", "1"] - , ["has_ero", "!=", "1"] - ]] - , ["release", "!=", ["and" - , ["official", "=", "1"] - , ["patch", "!=", "1"] - , ["has_ero", "=", "1"] - ]] - , ["tag", "=", "g2708"] - ]`} - , ["search", "=", "${title}"] - ], - "fields": "title, titles{title, lang}, released", - "sort": "searchrank", - "results": 20 - }`); - - const ret: MediaTypeModel[] = []; - for (const vn of vnData.results) { - ret.push( - new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: coerceYear( - vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear() : 0, - ), - dataSource: this.apiName, - id: vn.id, - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const vnData = await this.postVNQuery(`{ - "filters": ["id", "=", "${id}"], - "fields": "title, titles{title, lang}, devstatus, released, image{url, sexual}, rating, tags{name, category, rating, spoiler}, developers{name}" - }`); - - if (vnData.results.length !== 1) throw Error(`MDB | Expected 1 result from query, got ${vnData.results.length}.`); - const vn = vnData.results[0]; - const releasedIsDate = vn.released !== null && vn.released !== 'TBA'; - vn.released ??= 'Unknown'; - - const releaseData = await this.postReleaseQuery(`{ - "filters": ["and" - , ["vn", "=" - , ["id", "=", "${id}"] - ] - , ["official", "=", 1] - ], - "fields": "producers.name, producers.publisher, producers.developer", - "results": 100 - }`); - - return new GameModel({ - type: MediaType.Game, - title: vn.title, - englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: coerceYear(releasedIsDate ? new Date(vn.released).getFullYear() : vn.released), - dataSource: this.apiName, - url: `https://vndb.org/${vn.id}`, - id: vn.id, - - developers: vn.developers.map(d => d.name), - publishers: releaseData.results - .flatMap(r => r.producers) - .filter(p => p.publisher) - .sort((p1, p2) => Number(p2.developer) - Number(p1.developer)) // Place developer-publishers first in publisher list - .map(p => p.name) - .unique(), - genres: vn.tags - .filter(t => t.category === TagCategory.Content && t.spoiler === TagSpoiler.None && t.rating >= 2) - .sort((t1, t2) => t2.rating - t1.rating) - .map(t => t.name), - onlineRating: vn.rating ?? NaN, - // TODO: Ideally we should simply flag a sensitive image, then let the user handle it non-destructively - image: this.plugin.settings.sfwFilter && (vn.image?.sexual ?? 0) > 0.5 ? 'NSFW' : vn.image?.url, - - released: vn.devstatus === VNDevStatus.Finished, - releaseDate: releasedIsDate ? (this.plugin.dateFormatter.format(vn.released, this.apiDateFormat) ?? vn.released) : vn.released, - - userData: { - played: false, - personalRating: 0, - }, - }); - } - - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.VNDBAPI_disabledMediaTypes; - } -} diff --git a/api/apis/WikipediaAPI.ts b/api/apis/WikipediaAPI.ts deleted file mode 100644 index d4a62bcc..00000000 --- a/api/apis/WikipediaAPI.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type MediaDbPlugin from '../../main'; -import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { WikiModel } from '../../models/WikiModel'; -import { MediaType } from '../../utils/MediaType'; -import { APIModel } from '../APIModel'; - -interface SearchResponse { - query: { - search: { - title: string; - pageid: number; - }[]; - }; -} - -interface IdResponse { - query: { - pages: Record; - }; -} - -interface WikipediaPage { - pageid: number; - title: string; - contentmodel: string; - pagelanguage: string; - pagelanguagehtmlcode: string; - pagelanguagedir: string; - touched: string; // ISO date string - lastrevid: number; - length: number; - fullurl: string; - editurl: string; - canonicalurl: string; -} -export class WikipediaAPI extends APIModel { - plugin: MediaDbPlugin; - apiDateFormat: string = 'YYYY-MM-DDTHH:mm:ssZ'; // ISO - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'Wikipedia API'; - this.apiDescription = 'The API behind Wikipedia'; - this.apiUrl = 'https://www.wikipedia.com'; - this.types = [MediaType.Wiki]; - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - // console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as SearchResponse; - console.debug(data); - const ret: MediaTypeModel[] = []; - - for (const result of data.query.search) { - ret.push( - new WikiModel({ - type: 'wiki', - title: result.title, - englishTitle: result.title, - year: 0, - dataSource: this.apiName, - id: result.pageid.toString(), - }), - ); - } - - return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&prop=info&pageids=${encodeURIComponent(id)}&inprop=url&format=json&origin=*`; - const fetchData = await fetch(searchUrl); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from ${this.apiName}.`); - } - - const data = (await fetchData.json()) as IdResponse; - // console.debug(data); - const result = Object.values(data?.query?.pages)[0]; - - return new WikiModel({ - title: result.title, - englishTitle: result.title, - dataSource: this.apiName, - url: result.fullurl, - id: result.pageid.toString(), - - wikiUrl: result.fullurl, - lastUpdated: this.plugin.dateFormatter.format(result.touched, this.apiDateFormat), - length: result.length, - - userData: {}, - }); - } - getDisabledMediaTypes(): MediaType[] { - return this.plugin.settings.WikipediaAPI_disabledMediaTypes; - } -} diff --git a/api/geniusLyricsExtract.ts b/api/geniusLyricsExtract.ts deleted file mode 100644 index 000e9218..00000000 --- a/api/geniusLyricsExtract.ts +++ /dev/null @@ -1,87 +0,0 @@ -const LYRICS_CONTAINER_OPEN_RE = - /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; - -function stripHtmlToPlainLyrics(fragment: string): string { - return fragment - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n') - .replace(/<[^>]+>/g, '') - .replace(/\n{3,}/g, '\n\n') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .trim(); -} - -/** Parses nested
blocks; the naive `*?
` regex stops at the first inner close tag. */ -function extractBalancedDivInnerHtml(html: string, contentStart: number): string { - let depth = 1; - let i = contentStart; - const openRe = //gi; - while (depth > 0) { - openRe.lastIndex = i; - closeRe.lastIndex = i; - const om = openRe.exec(html); - const cm = closeRe.exec(html); - if (!cm) { - break; - } - const oIdx = om ? om.index : Number.POSITIVE_INFINITY; - const cIdx = cm.index; - if (om && oIdx < cIdx) { - depth++; - i = om.index + om[0].length; - } else { - depth--; - if (depth === 0) { - return html.slice(contentStart, cIdx); - } - i = cm.index + cm[0].length; - } - } - return ''; -} - -function collectLyricsContainersRegex(html: string): string[] { - const chunks: string[] = []; - let m: RegExpExecArray | null; - LYRICS_CONTAINER_OPEN_RE.lastIndex = 0; - while ((m = LYRICS_CONTAINER_OPEN_RE.exec(html)) !== null) { - const inner = extractBalancedDivInnerHtml(html, m.index + m[0].length); - if (inner) { - chunks.push(inner); - } - } - return chunks; -} - -function extractOneContainerPlain(el: Element): string { - const clone = el.cloneNode(true) as Element; - clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(node => node.remove()); - return stripHtmlToPlainLyrics(clone.innerHTML); -} - -export function extractLyricsFromGeniusHtml(html: string): string { - let chunks: string[] = []; - try { - const doc = new DOMParser().parseFromString(html, 'text/html'); - doc.querySelectorAll('[data-lyrics-container="true"]').forEach(c => { - const plain = extractOneContainerPlain(c); - if (plain) { - chunks.push(plain); - } - }); - } catch { - chunks = []; - } - - if (chunks.length === 0) { - return '' - } - - return chunks.join('\n\n').replace(/\n{3,}/g, '\n\n').trim(); -} diff --git a/api/musicBrainzConstants.ts b/api/musicBrainzConstants.ts deleted file mode 100644 index bb218884..00000000 --- a/api/musicBrainzConstants.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MediaType } from '../utils/MediaType'; - -/** Stored on notes for any row backed by MusicBrainz (release, artist, or song). */ -export const MUSICBRAINZ_NOTE_DATA_SOURCE = 'MusicBrainz'; - -export function isMusicBrainzFamilyDataSource(dataSource: string): boolean { - return dataSource.contains('MusicBrainz'); -} - -/** Which registered API implements getById for this media type. */ -export function musicBrainzRegisteredApiName(mediaType: MediaType): 'MusicBrainz API' | 'MusicBrainz Artist API' | undefined { - if (mediaType === MediaType.Artist) { - return 'MusicBrainz Artist API'; - } - if (mediaType === MediaType.MusicRelease || mediaType === MediaType.Song) { - return 'MusicBrainz API'; - } - return undefined; -} diff --git a/api/schemas/GiantBomb.json b/api/schemas/GiantBomb.json deleted file mode 100644 index 4b8a13c3..00000000 --- a/api/schemas/GiantBomb.json +++ /dev/null @@ -1,11226 +0,0 @@ -{ - "components": { - "parameters": { - "FieldList": { - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "$ref": "#/components/schemas/FieldList" - } - }, - "Filter": { - "description": "The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format)", - "in": "query", - "name": "filter", - "required": false, - "schema": { - "$ref": "#/components/schemas/Filter" - } - }, - "Format": { - "description": "The data format of the response takes either xml, json, or jsonp.", - "in": "query", - "name": "format", - "required": false, - "schema": { - "$ref": "#/components/schemas/Format" - } - }, - "Game": { - "description": "Filter by the ID field on the game resource.", - "in": "query", - "name": "game", - "required": false, - "schema": { - "$ref": "#/components/schemas/GameId" - } - }, - "Limit": { - "description": "The number of results to display per page. This value defaults to 100 and can not exceed this number.", - "in": "query", - "name": "limit", - "required": false, - "schema": { - "$ref": "#/components/schemas/Limit" - } - }, - "Offset": { - "description": "Return results starting with the object at the offset specified.", - "in": "query", - "name": "offset", - "required": false, - "schema": { - "$ref": "#/components/schemas/Offset" - } - }, - "Page": { - "description": "Page number of search results.", - "in": "query", - "name": "page", - "required": false, - "schema": { - "$ref": "#/components/schemas/Page" - } - }, - "Platforms": { - "description": "Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by.", - "in": "query", - "name": "platforms", - "required": false, - "schema": { - "$ref": "#/components/schemas/PlatformId" - } - }, - "Query": { - "description": "The search string.", - "in": "query", - "name": "query", - "required": false, - "schema": { - "$ref": "#/components/schemas/Query" - } - }, - "Resources": { - "description": "List of resources to filter results. This filter can accept multiple arguments, each delimited with a \",\". Available options are:
game
franchise
character
concept
object
location
person
company
video", - "explode": false, - "in": "query", - "name": "resources", - "required": false, - "schema": { - "description": "##ENTER##", - "items": { - "$ref": "#/components/schemas/ResourceType" - }, - "type": "array" - }, - "style": "form" - }, - "Sort": { - "description": "The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc.", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "$ref": "#/components/schemas/Sort" - } - }, - "SubscriberOnly": { - "description": "##ENTER##", - "in": "query", - "name": "subscriber_only", - "required": false, - "schema": { - "$ref": "#/components/schemas/SubscriberOnly" - } - }, - "TimeToSave": { - "description": "The number of seconds into the video the current user is", - "in": "query", - "name": "time_to_save", - "required": false, - "schema": { - "$ref": "#/components/schemas/TimeToSave" - } - }, - "VideoId": { - "description": "Id of the video", - "in": "query", - "name": "video_id", - "required": false, - "schema": { - "$ref": "#/components/schemas/VideoId" - } - } - }, - "responses": { - "InvalidAPIKey": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidAPIKey" - } - }, - "application/jsonp": { - "schema": { - "$ref": "#/components/schemas/InvalidAPIKey" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/InvalidAPIKey" - } - } - }, - "description": "##ENTER##" - } - }, - "schemas": { - "Accessory": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the accessory detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the accessory was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the accessory was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the accessory.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the accessory on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Accessory.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Accessory" - }, - { - "properties": {} - } - ] - }, - "Character": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the character is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the character detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "birthday": { - "description": "Birthday of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the character was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the character was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appeared_in_game": { - "description": "Game where the character made its first appearance.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "gender": { - "description": "Gender of the character. Available options are: Male, Female, Other", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "last_name": { - "description": "Last name of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "real_name": { - "description": "Real name of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the character on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Character.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Character" - }, - { - "properties": { - "concepts": { - "description": "Concepts related to the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "enemies": { - "description": "Enemeis of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "franchises": { - "description": "Franchises related to the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "friends": { - "description": "Friends of the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "games": { - "description": "Games the character has appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the character.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the character.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Chat": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the chat detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "channel_name": { - "description": "Name of the video streaming channel associated with the chat.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the chat.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for chat.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the chat.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the chat.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "password": { - "description": "chat password.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the chat on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "title": { - "description": "Title of the chat.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Chat.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Chat" - }, - { - "properties": {} - } - ] - }, - "Company": { - "description": "##ENTER##", - "properties": { - "abbreviation": { - "description": "Abbreviation of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "aliases": { - "description": "List of aliases the company is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the company detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the company was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_founded": { - "description": "Date the company was founded.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the company was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "location_address": { - "description": "Street address of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "location_city": { - "description": "City the company resides in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "location_country": { - "description": "Country the company resides in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "location_state": { - "description": "State the company resides in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "phone": { - "description": "Phone number of the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the company on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "website": { - "description": "URL to the company website.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Company.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Company" - }, - { - "properties": { - "characters": { - "description": "Characters related to the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "developed_games": { - "description": "Games the company has developed.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "developer_releases": { - "description": "Releases the company has developed.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "distributor_releases": { - "description": "Releases the company has distributed.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "published_games": { - "description": "Games published by the company.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "publisher_releases": { - "description": "Releases the company has published.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Concept": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the concept is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the concept detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the concept was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the concept was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appeared_in_franchise": { - "description": "Franchise where the concept made its first appearance.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appeared_in_game": { - "description": "Game where the concept made its first appearance.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the concept on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Concept.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Concept" - }, - { - "properties": { - "characters": { - "description": "Characters related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "franchises": { - "description": "Franchises related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "games": { - "description": "Games the concept has appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the concept.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "related_concepts": { - "description": "Other concepts related to the concept.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "CurrentLive": { - "description": "##ENTER##", - "properties": { - "success": { - "description": "##ENTER##", - "type": "integer" - }, - "video": { - "description": "##ENTER##", - "nullable": true, - "properties": { - "image": { - "description": "Thumbnail image of the video", - "example": "##WRONG TYPE##", - "type": "string" - }, - "stream": { - "description": "URL of the stream (HLS format)", - "example": "##WRONG TYPE##", - "type": "string" - }, - "title": { - "description": "Title of the live video", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "Dlc": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the dlc detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the dlc was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the dlc was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "game": { - "description": "Game the dlc is for.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "platform": { - "description": "The dlc's platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "release_date": { - "description": "Date of the dlc.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the dlc on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Dlc.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Dlc" - }, - { - "properties": {} - } - ] - }, - "FieldList": { - "description": "##ENTER##", - "type": "array" - }, - "Filter": { - "description": "##ENTER##", - "pattern": "^((\\w+:((\\w+)|(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\|\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}))),?)+$", - "type": "string" - }, - "Format": { - "description": "##ENTER##", - "enum": ["xml", "json", "jsonp"], - "type": "string" - }, - "Franchise": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the franchise is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the franchise detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the franchise was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the franchise was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the franchise on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Franchise.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Franchise" - }, - { - "properties": { - "characters": { - "description": "Characters related to the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "games": { - "description": "Games the franchise has appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the franchise.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Game": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the game is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the game detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the game was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the game was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_day": { - "description": "Expected day of release. The month is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_month": { - "description": "Expected month of release. The month is represented numerically. Combine with 'expected_release_day', 'expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_quarter": { - "description": "Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_year": { - "description": "Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "number_of_user_reviews": { - "description": "Number of user reviews of the game on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "original_game_rating": { - "description": "Rating of the first release of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "original_release_date": { - "description": "Date the game was first released.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "platforms": { - "description": "The platforms the game exists on.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the game on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Game.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Game" - }, - { - "properties": { - "characters": { - "description": "Characters related to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "developers": { - "description": "Companies who developed the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "dlcs": { - "description": "Game DLCs", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appearance_characters": { - "description": "Characters that first appeared in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appearance_concepts": { - "description": "Concepts that first appeared in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appearance_locations": { - "description": "Locations that first appeared in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appearance_objects": { - "description": "Objects that first appeared in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appearance_people": { - "description": "People that were first credited in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "franchises": { - "description": "Franchises related to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "genres": { - "description": "Genres that encompass the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "images": { - "description": "List of images associated to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "killed_characters": { - "description": "Characters killed in the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "publishers": { - "description": "Companies who published the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "releases": { - "description": "Releases of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "reviews": { - "description": "Staff reviews of the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "similar_games": { - "description": "Other games similar to the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "themes": { - "description": "Themes that encompass the game.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "videos": { - "description": "Videos associated to the game.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "GameId": { - "description": "##ENTER##", - "type": "integer" - }, - "GameRating": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the game_rating detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for game_rating.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the game_rating.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the game_rating.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the game_rating.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "rating_board": { - "description": "Rating board that issues this game_rating.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "GameRating.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/GameRating" - }, - { - "properties": {} - } - ] - }, - "Genre": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the genre detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the genre was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the genre was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the genre.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the genre on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Genre.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Genre" - }, - { - "properties": {} - } - ] - }, - "GetAllSavedTime": { - "properties": { - "savedTimes": { - "description": "##ENTER##", - "items": { - "description": "##ENTER##", - "properties": { - "savedOn": { - "description": "Time/date the progress was saved", - "example": "##WRONG TYPE##", - "type": "string" - }, - "savedTime": { - "description": "Saved time for this video", - "example": "##WRONG TYPE##", - "type": "string" - }, - "videoId": { - "description": "Id of the video", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "success": { - "description": "##ENTER##", - "type": "integer" - } - }, - "type": "object" - }, - "GetSavedTime": { - "description": "##ENTER##", - "properties": { - "savedTime": { - "description": "Saved time of the video or -1 if no time is saved for this user", - "example": "##WRONG TYPE##", - "type": "string" - }, - "success": { - "description": "##ENTER##", - "type": "integer" - } - }, - "type": "object" - }, - "Image": { - "description": "##ENTER##", - "properties": { - "icon_url": { - "description": "URL to the icon version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tag": { - "description": "Name of image tag for filerting images.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "medium_url": { - "description": "URL to the medium size of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "original_url": { - "description": "URL to the original image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "screen_large_url": { - "description": "URL to the large screenshot version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "screen_url": { - "description": "URL to the screenshot version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "small_url": { - "description": "URL to the small version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "super_url": { - "description": "URL to the super sized version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "thumb_url": { - "description": "URL to the thumb-sized version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "tiny_url": { - "description": "URL to the tiny version of the image.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "InvalidAPIKey": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "error": { - "enum": ["Invalid API Key"] - }, - "results": { - "enum": [[]] - }, - "status_code": { - "enum": [100] - } - } - } - ] - }, - "Limit": { - "description": "##ENTER##", - "maximum": 100, - "minimum": 0, - "type": "integer" - }, - "Location": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the location is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the location detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the location was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the location was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appeared_in_game": { - "description": "Game where the location made its first appearance.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the location.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the location on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Location.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Location" - }, - { - "properties": {} - } - ] - }, - "Object": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the object is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the object detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the object was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the object was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_appeared_in_game": { - "description": "Game where the object made its first appearance.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the object on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Object.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Object" - }, - { - "properties": { - "characters": { - "description": "Characters related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "companies": { - "description": "Companies related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "franchises": { - "description": "Franchises related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "games": { - "description": "Games the object has appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the object.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the object.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Offset": { - "description": "##ENTER##", - "type": "integer" - }, - "Page": { - "description": "##ENTER##", - "type": "integer" - }, - "Person": { - "description": "##ENTER##", - "properties": { - "aliases": { - "description": "List of aliases the person is known by. A \\n (newline) seperates each alias.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the person detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "birth_date": { - "description": "Date the person was born.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "country": { - "description": "Country the person resides in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the person was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the person was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "death_date": { - "description": "Date the person died.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "first_credited_game": { - "description": "Game the person was first credited.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "gender": { - "description": "Gender of the person. Available options are: Male, Female, Other", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "hometown": { - "description": "City or town the person resides in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the person on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Person.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "properties": { - "characters": { - "description": "Characters related to the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "concepts": { - "description": "Concepts related to the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "franchises": { - "description": "Franchises related to the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "games": { - "description": "Games the person has appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "locations": { - "description": "Locations related to the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "objects": { - "description": "Objects related to the person.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "people": { - "description": "People who have worked with the person.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Platform": { - "description": "##ENTER##", - "properties": { - "abbreviation": { - "description": "Abbreviation of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the platform detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "company": { - "description": "Company that created the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the platform was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the platform was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image_tags": { - "description": "List of image tags to filter the images endpoint.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "install_base": { - "description": "Approximate number of sold hardware units.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "online_support": { - "description": "Flag indicating whether the platform has online capabilities.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "original_price": { - "description": "Initial price point of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "release_date": { - "description": "Date of the platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the platform on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Platform.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Platform" - }, - { - "properties": {} - } - ] - }, - "PlatformId": { - "description": "##ENTER##", - "type": "integer" - }, - "Promo": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the promo detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the promo was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the promo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for promo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the promo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the promo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "link": { - "description": "The link that promo points to.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the promo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "resource_type": { - "allOf": [ - { - "$ref": "#/components/schemas/ResourceType" - }, - { - "description": "The type of resource the promo is pointing towards." - } - ] - }, - "user": { - "description": "Author of the promo.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Promo.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Promo" - }, - { - "properties": {} - } - ] - }, - "Query": { - "description": "##ENTER##", - "type": "string" - }, - "RatingBoard": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the rating_board detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the rating_board was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the rating_board was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the rating_board.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the rating_board on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "RatingBoard.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/RatingBoard" - }, - { - "properties": { - "region": { - "description": "Region the rating_board is responsible for.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Region": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the region detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the region was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the region was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the region.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the region on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Region.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Region" - }, - { - "properties": { - "rating_boards": { - "description": "region in the region.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "Release": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the release detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the release was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the release was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_day": { - "description": "\"Expected day of release. The day is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.\"", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_month": { - "description": "\"Expected month of release. The month is represented numerically. Combine with 'expected_release_day', expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.\"", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_quarter": { - "description": "Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "expected_release_year": { - "description": "Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'release_date' field is set.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "game": { - "description": "Game the release is for.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "game_rating": { - "description": "Rating of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "maximum_players": { - "description": "Maximum players", - "example": "##WRONG TYPE##", - "type": "string" - }, - "minimum_players": { - "description": "Minimum players", - "example": "##WRONG TYPE##", - "type": "string" - }, - "multiplayer_features": { - "description": "Multiplayer features", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "platform": { - "description": "The release's platform.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "product_code_type": { - "description": "The type of product code the release has (ex. UPC/A, ISBN-10, EAN-13).", - "example": "##WRONG TYPE##", - "type": "string" - }, - "product_code_value": { - "description": "The release's product code.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "region": { - "description": "Region the release is responsible for.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "release_date": { - "description": "Date of the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "resolutions": { - "description": "Resolutions available", - "example": "##WRONG TYPE##", - "type": "string" - }, - "singleplayer_features": { - "description": "Singleplayer features", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the release on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "sound_systems": { - "description": "Sound systems", - "example": "##WRONG TYPE##", - "type": "string" - }, - "widescreen_support": { - "description": "Widescreen support", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Release.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Release" - }, - { - "properties": { - "developers": { - "description": "Companies who developed the release.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "publishers": { - "description": "Companies who published the release.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "ResourceType": { - "description": "##ENTER##", - "enum": ["game", "franchise", "character", "concept", "object", "location", "person", "company", "video"], - "type": "string" - }, - "Response": { - "description": "##ENTER##", - "properties": { - "error": { - "description": "A text string representing the status_code", - "type": "string" - }, - "limit": { - "description": "The value of the limit filter specified, or 100 if not specified", - "type": "integer" - }, - "number_of_page_results": { - "description": "The number of results on this page", - "type": "integer" - }, - "number_of_total_results": { - "description": "The number of total results matching the filter conditions specified", - "type": "integer" - }, - "offset": { - "description": "The value of the offset filter specified, or 0 if not specified", - "type": "integer" - }, - "results": { - "description": "Zero or more items that match the filters specified", - "type": "string" - }, - "status_code": { - "description": "An integer indicating the result of the request. Acceptable values are:
1:OK
100:Invalid API Key
101:Object Not Found
102:Error in URL Format
103:'jsonp' format requires a 'json_callback' argument
104:Filter Error
105:Subscriber only video is for subscribers only", - "type": "integer" - } - }, - "type": "object", - "xml": { - "name": "response", - "wrapped": true - } - }, - "Review": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the review detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "dlc_name": { - "description": "Name of the Downloadable Content package.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "game": { - "description": "Game the review is for.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "publish_date": { - "description": "Date the review was published on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "release": { - "description": "Release of game for review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "reviewer": { - "description": "Name of the review's author.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "score": { - "description": "The score given to the game on a scale of 1 to 5.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the review on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Review.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Review" - }, - { - "properties": { - "platforms": { - "description": "Platforms the review appeared in.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "SaveTime": { - "description": "##ENTER##", - "properties": { - "message": { - "description": "##ENTER##", - "type": "string" - }, - "success": { - "description": "##ENTER##", - "type": "boolean" - } - }, - "type": "object" - }, - "Search": { - "allOf": [ - { - "oneOf": [ - { - "$ref": "#/components/schemas/Game" - }, - { - "$ref": "#/components/schemas/Franchise" - }, - { - "$ref": "#/components/schemas/Character" - }, - { - "$ref": "#/components/schemas/Concept" - }, - { - "$ref": "#/components/schemas/Object" - }, - { - "$ref": "#/components/schemas/Location" - }, - { - "$ref": "#/components/schemas/Person" - }, - { - "$ref": "#/components/schemas/Company" - }, - { - "$ref": "#/components/schemas/Video" - } - ] - }, - { - "description": "##ENTER##", - "properties": { - "resource_type": { - "allOf": [ - { - "$ref": "#/components/schemas/ResourceType" - }, - { - "description": "The type of resource the result is mapped to. Available options are:
game
franchise
character
concept
object
location
person
company
video" - } - ] - } - }, - "type": "object" - } - ] - }, - "Sort": { - "description": "##ENTER##", - "pattern": "^\\w+:((asc)|(desc))$", - "type": "string" - }, - "SubscriberOnly": { - "description": "##ENTER##", - "type": "boolean" - }, - "Theme": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the theme detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for theme.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the theme.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the theme.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the theme on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Theme.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Theme" - }, - { - "properties": {} - } - ] - }, - "TimeToSave": { - "description": "##ENTER##", - "type": "integer" - }, - "Type": { - "description": "##ENTER##", - "properties": { - "detail_resource_name": { - "description": "The name of the type's detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the type.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "list_resource_name": { - "description": "The name of the type's list resource.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "UserReview": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the user_review detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_added": { - "description": "Date the user_review was added to Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "date_last_updated": { - "description": "Date the user_review was last updated on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the user_review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "description": { - "description": "Description of the user_review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "dlc": { - "description": "DLC being reviewed.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "game": { - "description": "Game being reviewd.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for user_review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the user_review.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "release": { - "description": "Release being reviewed.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "reviewer": { - "description": "Name of the review's author.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "score": { - "description": "The score given to the game on a scale of 1 to 5.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the user_review on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "UserReview.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/UserReview" - }, - { - "properties": {} - } - ] - }, - "Video": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the video detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "associations": { - "description": "Related objects to the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "embed_player": { - "description": "URL for video embed player. To be inserted into an iFrame.
You can add ?autoplay=true to auto-play.
You can add ?time=x where 'x' is an integer between 0 and the length of the video in seconds to start the video at that point.
You can add ?vol=x where 'x' is a decimal between 0 and 1, .75 for example, to set the starting volume.
The above three parameters may be used together. Example: ?time=45&vol=.5&autoplay=true
See http://www.giantbomb.com/api/video-embed-sample/ for more information on using the embed player.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "hd_url": { - "description": "URL to the HD version of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "high_url": { - "description": "URL to the High Res version of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "length_seconds": { - "description": "Length (in seconds) of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "low_url": { - "description": "URL to the Low Res version of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "premium": { - "description": "Premium status of video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "publish_date": { - "description": "Date the video was published on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "saved_time": { - "description": "The time where the user left off watching this video", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the video on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "url": { - "description": "The video's filename.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "user": { - "description": "Author of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "video_categories": { - "description": "Video categories", - "example": "##WRONG TYPE##", - "type": "string" - }, - "video_show": { - "description": "Video show", - "example": "##WRONG TYPE##", - "type": "string" - }, - "video_type": { - "description": "Video category", - "example": "##WRONG TYPE##", - "type": "string" - }, - "youtube_id": { - "description": "Youtube ID for the video.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "Video.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/Video" - }, - { - "properties": { - "crew": { - "description": "Crew of the video.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "hosts": { - "description": "Hosts of the video.", - "example": "##WRONG TYPE##", - "type": "string" - } - } - } - ] - }, - "VideoCategory": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the video_category detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the video_category.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the video_category.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the video_category.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the video_category.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the video_category on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "VideoCategory.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoCategory" - }, - { - "properties": {} - } - ] - }, - "VideoId": { - "description": "##ENTER##", - "type": "integer" - }, - "VideoShow": { - "description": "##ENTER##", - "properties": { - "active": { - "description": "Is this show currently active", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_detail_url": { - "description": "URL pointing to the video_show detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "api_videos_url": { - "description": "Endpoint to retrieve the videos attached to this video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "display_nav": { - "description": "Should this show be displayed in navigation menus", - "example": "##WRONG TYPE##", - "type": "string" - }, - "guid": { - "description": "For use in single item api call for video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "image": { - "description": "Main image of the video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "latest": { - "description": "The latest episode of a video show. Overrides other sorts when used as a sort field.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "logo": { - "description": "Show logo.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "position": { - "description": "Editor ordering.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "premium": { - "description": "Premium status of video_show.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the video_show on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "title": { - "description": "Title of the video_show.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "VideoShow.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoShow" - }, - { - "properties": {} - } - ] - }, - "VideoType": { - "description": "##ENTER##", - "properties": { - "api_detail_url": { - "description": "URL pointing to the video_type detail resource.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "deck": { - "description": "Brief summary of the video_type.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "id": { - "description": "Unique ID of the video_type.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "name": { - "description": "Name of the video_type.", - "example": "##WRONG TYPE##", - "type": "string" - }, - "site_detail_url": { - "description": "URL pointing to the video_type on Giant Bomb.", - "example": "##WRONG TYPE##", - "type": "string" - } - }, - "type": "object" - }, - "VideoType.Detail": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoType" - }, - { - "properties": {} - } - ] - } - }, - "securitySchemes": { - "api_key": { - "in": "query", - "name": "api_key", - "type": "apiKey" - } - } - }, - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/" - }, - "info": { - "description": "##ENTER##", - "title": "Giant Bomb API", - "version": "0.7" - }, - "openapi": "3.0.2", - "paths": { - "/accessories": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Accessory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Accessory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Accessory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/accessory/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Accessory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Accessory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Accessory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/character/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "birthday", - "concepts", - "date_added", - "date_last_updated", - "deck", - "description", - "enemies", - "first_appeared_in_game", - "franchises", - "friends", - "games", - "gender", - "guid", - "id", - "image", - "image_tags", - "last_name", - "locations", - "name", - "objects", - "people", - "real_name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Character.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Character.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Character.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/characters": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "birthday", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_game", - "gender", - "guid", - "id", - "image", - "image_tags", - "last_name", - "name", - "real_name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Character" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Character" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Character" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/chat/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "channel_name", "deck", "guid", "id", "image", "password", "site_detail_url", "title"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Chat.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Chat.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Chat.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Live"] - }, - "summary": "##ENTER##" - }, - "/chats": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "channel_name", "deck", "guid", "id", "image", "password", "site_detail_url", "title"], - "type": "string" - } - } - ] - }, - "style": "form" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Chat" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Chat" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Chat" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Live"] - }, - "summary": "##ENTER##" - }, - "/companies": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "abbreviation", - "aliases", - "api_detail_url", - "date_added", - "date_founded", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "location_address", - "location_city", - "location_country", - "location_state", - "name", - "phone", - "site_detail_url", - "website" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Company" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Company" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Company" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/company/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "abbreviation", - "aliases", - "api_detail_url", - "characters", - "concepts", - "date_added", - "date_founded", - "date_last_updated", - "deck", - "description", - "developed_games", - "developer_releases", - "distributor_releases", - "guid", - "id", - "image", - "image_tags", - "location_address", - "location_city", - "location_country", - "location_state", - "locations", - "name", - "objects", - "people", - "phone", - "published_games", - "publisher_releases", - "site_detail_url", - "website" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Company.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Company.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Company.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/concept/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "characters", - "concepts", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_franchise", - "first_appeared_in_game", - "franchises", - "games", - "guid", - "id", - "image", - "image_tags", - "locations", - "name", - "objects", - "people", - "related_concepts", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Concept.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Concept.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Concept.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/concepts": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_franchise", - "first_appeared_in_game", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Concept" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Concept" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Concept" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/dlc/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "game", - "guid", - "id", - "image", - "name", - "platform", - "release_date", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Dlc.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Dlc.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Dlc.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/dlcs": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "game", - "guid", - "id", - "image", - "name", - "platform", - "release_date", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Platforms" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Dlc" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Dlc" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Dlc" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/franchise/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "characters", - "concepts", - "date_added", - "date_last_updated", - "deck", - "description", - "games", - "guid", - "id", - "image", - "image_tags", - "locations", - "name", - "objects", - "people", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Franchise.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Franchise.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Franchise.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/franchises": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Franchise" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Franchise" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Franchise" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/game_rating/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "guid", "id", "image", "name", "rating_board"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/GameRating.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/GameRating.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/GameRating.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/game_ratings": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "guid", "id", "image", "name", "rating_board"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/GameRating" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/GameRating" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/GameRating" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/game/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "characters", - "concepts", - "date_added", - "date_last_updated", - "deck", - "description", - "developers", - "dlcs", - "expected_release_day", - "expected_release_month", - "expected_release_quarter", - "expected_release_year", - "first_appearance_characters", - "first_appearance_concepts", - "first_appearance_locations", - "first_appearance_objects", - "first_appearance_people", - "franchises", - "genres", - "guid", - "id", - "image", - "image_tags", - "images", - "killed_characters", - "locations", - "name", - "number_of_user_reviews", - "objects", - "original_game_rating", - "original_release_date", - "people", - "platforms", - "publishers", - "releases", - "reviews", - "similar_games", - "site_detail_url", - "themes", - "videos" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Game.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Game.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Game.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/games": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "expected_release_day", - "expected_release_month", - "expected_release_quarter", - "expected_release_year", - "guid", - "id", - "image", - "image_tags", - "name", - "number_of_user_reviews", - "original_game_rating", - "original_release_date", - "platforms", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Platforms" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Game" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Game" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Game" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/genre/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Genre.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Genre.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Genre.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/genres": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Genre" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Genre" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Genre" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/images/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "icon_url", - "image_tag", - "medium_url", - "original_url", - "screen_large_url", - "screen_url", - "small_url", - "super_url", - "thumb_url", - "tiny_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Filter" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Image" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Image" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Image" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/location/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_game", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Location.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Location.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Location.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/locations": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_game", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Location" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Location" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Location" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/object/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "characters", - "companies", - "concepts", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_game", - "franchises", - "games", - "guid", - "id", - "image", - "image_tags", - "locations", - "name", - "objects", - "people", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Object.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Object.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Object.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/objects": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "first_appeared_in_game", - "guid", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Object" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Object" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Object" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/people": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "birth_date", - "country", - "date_added", - "date_last_updated", - "death_date", - "deck", - "description", - "first_credited_game", - "gender", - "guid", - "hometown", - "id", - "image", - "image_tags", - "name", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/person/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "aliases", - "api_detail_url", - "birth_date", - "characters", - "concepts", - "country", - "date_added", - "date_last_updated", - "death_date", - "deck", - "description", - "first_credited_game", - "franchises", - "games", - "gender", - "guid", - "hometown", - "id", - "image", - "image_tags", - "locations", - "name", - "objects", - "people", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Person.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Person.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Person.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/platform/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "abbreviation", - "api_detail_url", - "company", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "install_base", - "name", - "online_support", - "original_price", - "release_date", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Platform.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Platform.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Platform.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/platforms": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "abbreviation", - "api_detail_url", - "company", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "image_tags", - "install_base", - "name", - "online_support", - "original_price", - "release_date", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Platform" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Platform" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Platform" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/promo/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "deck", "guid", "id", "image", "link", "name", "resource_type", "user"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Promo.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Promo.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Promo.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["General"] - }, - "summary": "##ENTER##" - }, - "/promos": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "deck", "guid", "id", "image", "link", "name", "resource_type", "user"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Promo" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Promo" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Promo" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["General"] - }, - "summary": "##ENTER##" - }, - "/rating_board/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "name", - "region", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/RatingBoard.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/RatingBoard.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/RatingBoard.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/rating_boards": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/RatingBoard" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/RatingBoard" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/RatingBoard" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/region/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "guid", - "id", - "image", - "name", - "rating_boards", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Region.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Region.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Region.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/regions": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "date_added", "date_last_updated", "deck", "description", "guid", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Region" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Region" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Region" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/release/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "developers", - "expected_release_day", - "expected_release_month", - "expected_release_quarter", - "expected_release_year", - "game", - "game_rating", - "guid", - "id", - "image", - "maximum_players", - "minimum_players", - "multiplayer_features", - "name", - "platform", - "product_code_type", - "product_code_value", - "publishers", - "region", - "release_date", - "resolutions", - "singleplayer_features", - "site_detail_url", - "sound_systems", - "widescreen_support" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Release.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Release.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Release.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/releases": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "expected_release_day", - "expected_release_month", - "expected_release_quarter", - "expected_release_year", - "game", - "game_rating", - "guid", - "id", - "image", - "maximum_players", - "minimum_players", - "multiplayer_features", - "name", - "platform", - "product_code_type", - "product_code_value", - "region", - "release_date", - "resolutions", - "singleplayer_features", - "site_detail_url", - "sound_systems", - "widescreen_support" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Platforms" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Release" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Release" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Release" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/review/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "deck", - "description", - "dlc_name", - "game", - "guid", - "id", - "platforms", - "publish_date", - "release", - "reviewer", - "score", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Review.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Review.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Review.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Reviews"] - }, - "summary": "##ENTER##" - }, - "/reviews": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "deck", - "description", - "dlc_name", - "game", - "guid", - "id", - "publish_date", - "release", - "reviewer", - "score", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Review" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Review" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Review" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Reviews"] - }, - "summary": "##ENTER##" - }, - "/search": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "abbreviation", - "aliases", - "api_detail_url", - "associations", - "birth_date", - "birthday", - "country", - "date_added", - "date_founded", - "date_last_updated", - "death_date", - "deck", - "description", - "embed_player", - "expected_release_day", - "expected_release_month", - "expected_release_quarter", - "expected_release_year", - "first_appeared_in_franchise", - "first_appeared_in_game", - "first_credited_game", - "gender", - "guid", - "hd_url", - "high_url", - "hometown", - "id", - "image", - "image_tags", - "last_name", - "length_seconds", - "location_address", - "location_city", - "location_country", - "location_state", - "low_url", - "name", - "number_of_user_reviews", - "original_game_rating", - "original_release_date", - "phone", - "platforms", - "premium", - "publish_date", - "real_name", - "resource_type", - "saved_time", - "site_detail_url", - "url", - "user", - "video_categories", - "video_show", - "video_type", - "website", - "youtube_id" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "The number of results to display per page. This value defaults to 10 and can not exceed this number.", - "in": "query", - "name": "limit", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Limit" - }, - { - "description": "The number of results to display per page. This value defaults to 10 and can not exceed this number.", - "maximum": 10 - } - ] - } - }, - { - "$ref": "#/components/parameters/Page" - }, - { - "$ref": "#/components/parameters/Query" - }, - { - "$ref": "#/components/parameters/Resources" - }, - { - "$ref": "#/components/parameters/SubscriberOnly" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Search" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Search" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Search" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Search"] - }, - "summary": "##ENTER##" - }, - "/theme/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "guid", "id", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Theme.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Theme.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Theme.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/themes": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "guid", "id", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Theme" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Theme" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Theme" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Wiki"] - }, - "summary": "##ENTER##" - }, - "/types": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Type" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Type" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Type" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["General"] - }, - "summary": "##ENTER##" - }, - "/user_review/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "dlc", - "game", - "guid", - "id", - "release", - "reviewer", - "score", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/UserReview.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/UserReview.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/UserReview.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Reviews"] - }, - "summary": "##ENTER##" - }, - "/user_reviews": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "date_added", - "date_last_updated", - "deck", - "description", - "dlc", - "game", - "guid", - "id", - "release", - "reviewer", - "score", - "site_detail_url" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Game" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/UserReview" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/UserReview" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/UserReview" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Reviews"] - }, - "summary": "##ENTER##" - }, - "/video_categories": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "deck", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Sort" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoCategory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoCategory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoCategory" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video_category/{id}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "deck", "id", "image", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "id", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoCategory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoCategory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoCategory.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video_show/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "active", - "api_detail_url", - "api_videos_url", - "deck", - "display_nav", - "guid", - "id", - "image", - "latest", - "logo", - "position", - "premium", - "site_detail_url", - "title" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoShow.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoShow.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoShow.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video_shows": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "active", - "api_detail_url", - "api_videos_url", - "deck", - "display_nav", - "guid", - "id", - "image", - "latest", - "logo", - "position", - "premium", - "site_detail_url", - "title" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoShow" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoShow" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoShow" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video_type/{id}": { - "description": "##ENTER##", - "get": { - "deprecated": true, - "description": "##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "deck", "id", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "id", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoType.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoType.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/VideoType.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Get a details of a video type", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video_types": { - "description": "##ENTER##", - "get": { - "deprecated": true, - "description": "##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": ["api_detail_url", "deck", "id", "name", "site_detail_url"], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoType" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoType" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/VideoType" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Get a list of video types", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video/{guid}": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "associations", - "crew", - "deck", - "embed_player", - "guid", - "hd_url", - "high_url", - "hosts", - "id", - "image", - "length_seconds", - "low_url", - "name", - "premium", - "publish_date", - "saved_time", - "site_detail_url", - "url", - "user", - "video_categories", - "video_show", - "video_type", - "youtube_id" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "description": "##ENTER##", - "in": "path", - "name": "guid", - "required": true, - "schema": { - "description": "##ENTER##", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Video.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Video.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "$ref": "#/components/schemas/Video.Detail" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - }, - "/video/current-live": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CurrentLive" - } - }, - "application/jsonp": { - "schema": { - "$ref": "#/components/schemas/CurrentLive" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/CurrentLive" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Get the currently running live stream", - "tags": ["Live"] - }, - "summary": "##ENTER##" - }, - "/video/get-all-saved-times": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetAllSavedTime" - } - }, - "application/jsonp": { - "schema": { - "$ref": "#/components/schemas/GetAllSavedTime" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/GetAllSavedTime" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Get all the video saved times for the user", - "tags": ["Bookmarks"] - }, - "summary": "##ENTER##" - }, - "/video/get-saved-time": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "$ref": "#/components/parameters/VideoId" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSavedTime" - } - }, - "application/jsonp": { - "schema": { - "$ref": "#/components/schemas/GetSavedTime" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/GetSavedTime" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Get the saved time of a video for the user", - "tags": ["Bookmarks"] - }, - "summary": "##ENTER##" - }, - "/video/save-time": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "$ref": "#/components/parameters/VideoId" - }, - { - "$ref": "#/components/parameters/TimeToSave" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SaveTime" - } - }, - "application/jsonp": { - "schema": { - "$ref": "#/components/schemas/SaveTime" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/SaveTime" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "Save the progress time of a video for this user", - "tags": ["Bookmarks"] - }, - "summary": "##ENTER##" - }, - "/videos": { - "description": "##ENTER##", - "get": { - "description": "##ENTER##", - "externalDocs": { - "description": "##ENTER##", - "url": "https://www.giantbomb.com/api/documentation/#ADD" - }, - "parameters": [ - { - "$ref": "#/components/parameters/Format" - }, - { - "description": "List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a \",\"", - "explode": false, - "in": "query", - "name": "field_list", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/FieldList" - }, - { - "items": { - "enum": [ - "api_detail_url", - "associations", - "deck", - "embed_player", - "guid", - "hd_url", - "high_url", - "id", - "image", - "length_seconds", - "low_url", - "name", - "premium", - "publish_date", - "saved_time", - "site_detail_url", - "url", - "user", - "video_categories", - "video_show", - "video_type", - "youtube_id" - ], - "type": "string" - } - } - ] - }, - "style": "form" - }, - { - "$ref": "#/components/parameters/Limit" - }, - { - "$ref": "#/components/parameters/Offset" - }, - { - "$ref": "#/components/parameters/Sort" - }, - { - "$ref": "#/components/parameters/SubscriberOnly" - }, - { - "$ref": "#/components/parameters/Filter" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Video" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/jsonp": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Video" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - }, - "application/xml": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Response" - }, - { - "properties": { - "results": { - "items": { - "$ref": "#/components/schemas/Video" - }, - "type": "array" - }, - "version": { - "description": "##ENTER##", - "example": "1.0", - "type": "string" - } - }, - "type": "object" - } - ], - "type": "object" - } - } - }, - "description": "##ENTER##" - }, - "401": { - "$ref": "#/components/responses/InvalidAPIKey" - } - }, - "security": [ - { - "api_key": [] - } - ], - "summary": "##ENTER##", - "tags": ["Videos"] - }, - "summary": "##ENTER##" - } - }, - "servers": [ - { - "description": "##ENTER##", - "url": "http://www.giantbomb.com/api/" - } - ], - "tags": [ - { - "description": "##ENTER##", - "name": "General" - }, - { - "description": "##ENTER##", - "name": "Wiki" - }, - { - "description": "##ENTER##", - "name": "Search" - }, - { - "description": "##ENTER##", - "name": "Reviews" - }, - { - "description": "##ENTER##", - "name": "Videos" - }, - { - "description": "##ENTER##", - "name": "Live" - }, - { - "description": "##ENTER##", - "name": "Bookmarks" - } - ] -} diff --git a/api/schemas/GiantBomb.ts b/api/schemas/GiantBomb.ts deleted file mode 100644 index 9d8f4ee8..00000000 --- a/api/schemas/GiantBomb.ts +++ /dev/null @@ -1,6454 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/accessories': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Accessory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Accessory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Accessory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/accessory/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Accessory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Accessory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Accessory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/character/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Character.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Character.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Character.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Character'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Character'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Character'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/chat/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Chat.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Chat.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Chat.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/chats': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Chat'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Chat'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Chat'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/companies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Company'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Company'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Company'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/company/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Company.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Company.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Company.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/concept/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Concept.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Concept.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Concept.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/concepts': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Concept'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Concept'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Concept'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/dlc/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Dlc.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Dlc.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Dlc.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/dlcs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ - platforms?: components['parameters']['Platforms']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Dlc'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Dlc'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Dlc'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/franchise/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Franchise.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Franchise.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Franchise.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/franchises': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Franchise'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Franchise'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Franchise'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/game_rating/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['GameRating.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['GameRating.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['GameRating.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/game_ratings': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['GameRating'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['GameRating'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['GameRating'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/game/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Game.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Game.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Game.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/games': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ - platforms?: components['parameters']['Platforms']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Game'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Game'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Game'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/genre/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Genre.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Genre.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Genre.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/genres': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Genre'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Genre'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Genre'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/images/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Image'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Image'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Image'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/location/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Location.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Location.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Location.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/locations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Location'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Location'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Location'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/object/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Object.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Object.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Object.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/objects': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Object'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Object'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Object'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Person'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Person'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Person'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/person/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Person.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Person.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Person.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/platform/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Platform.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Platform.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Platform.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/platforms': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Platform'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Platform'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Platform'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/promo/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Promo.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Promo.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Promo.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/promos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Promo'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Promo'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Promo'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/rating_board/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/rating_boards': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['RatingBoard'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/region/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Region.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Region.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Region.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/regions': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Region'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Region'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Region'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/release/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Release.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Release.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Release.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/releases': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ - platforms?: components['parameters']['Platforms']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Release'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Release'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Release'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/review/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Review.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Review.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Review.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Review'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Review'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Review'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/search': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 10 and can not exceed this number. */ - limit?: components['schemas']['Limit'] & unknown; - /** @description Page number of search results. */ - page?: components['parameters']['Page']; - /** @description The search string. */ - query?: components['parameters']['Query']; - /** @description List of resources to filter results. This filter can accept multiple arguments, each delimited with a ",". Available options are:
game
franchise
character
concept
object
location
person
company
video */ - resources?: components['parameters']['Resources']; - /** @description ##ENTER## */ - subscriber_only?: components['parameters']['SubscriberOnly']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Search'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Search'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Search'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/theme/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Theme.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Theme.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Theme.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/themes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Theme'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Theme'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Theme'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/types': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Type'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Type'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Type'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/user_review/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['UserReview.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['UserReview.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['UserReview.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/user_reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description Filter by the ID field on the game resource. */ - game?: components['parameters']['Game']; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['UserReview'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['UserReview'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['UserReview'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_categories': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_category/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoCategory.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_show/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_shows': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoShow'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_type/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a details of a video type - * @deprecated - * @description ##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoType.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoType.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoType.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video_types': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a list of video types - * @deprecated - * @description ##ENTER##
DEPRECATED: Please use the video_category or the video_show endpoint - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['VideoType'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['VideoType'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['VideoType'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video/{guid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - }; - header?: never; - path: { - /** @description ##ENTER## */ - guid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Video.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Video.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Video.Detail']; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video/current-live': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get the currently running live stream - * @description ##ENTER## - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CurrentLive']; - 'application/jsonp': components['schemas']['CurrentLive']; - 'application/xml': components['schemas']['CurrentLive']; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video/get-all-saved-times': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all the video saved times for the user - * @description ##ENTER## - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAllSavedTime']; - 'application/jsonp': components['schemas']['GetAllSavedTime']; - 'application/xml': components['schemas']['GetAllSavedTime']; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video/get-saved-time': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get the saved time of a video for the user - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description Id of the video */ - video_id?: components['parameters']['VideoId']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetSavedTime']; - 'application/jsonp': components['schemas']['GetSavedTime']; - 'application/xml': components['schemas']['GetSavedTime']; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/video/save-time': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Save the progress time of a video for this user - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description Id of the video */ - video_id?: components['parameters']['VideoId']; - /** @description The number of seconds into the video the current user is */ - time_to_save?: components['parameters']['TimeToSave']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SaveTime']; - 'application/jsonp': components['schemas']['SaveTime']; - 'application/xml': components['schemas']['SaveTime']; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * ##ENTER## - * @description ##ENTER## - */ - get: { - parameters: { - query?: { - /** @description The data format of the response takes either xml, json, or jsonp. */ - format?: components['parameters']['Format']; - /** @description List of field names to include in the response. Use this if you want to reduce the size of the response payload. This filter can accept multiple arguments, each delimited with a "," */ - field_list?: components['schemas']['FieldList'] & unknown; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - limit?: components['parameters']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - offset?: components['parameters']['Offset']; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - sort?: components['parameters']['Sort']; - /** @description ##ENTER## */ - subscriber_only?: components['parameters']['SubscriberOnly']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - filter?: components['parameters']['Filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description ##ENTER## */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Response'] & { - results?: components['schemas']['Video'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/jsonp': components['schemas']['Response'] & { - results?: components['schemas']['Video'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - 'application/xml': components['schemas']['Response'] & { - results?: components['schemas']['Video'][]; - /** - * @description ##ENTER## - * @example 1.0 - */ - version?: string; - }; - }; - }; - 401: components['responses']['InvalidAPIKey']; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** @description ##ENTER## */ - Accessory: { - /** - * @description URL pointing to the accessory detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the accessory was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the accessory was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the accessory. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the accessory. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for accessory. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the accessory. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the accessory. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the accessory. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the accessory on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Accessory.Detail': components['schemas']['Accessory'] & unknown; - /** @description ##ENTER## */ - Character: { - /** - * @description List of aliases the character is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the character detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Birthday of the character. - * @example ##WRONG TYPE## - */ - birthday?: string; - /** - * @description Date the character was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the character was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the character. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the character. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Game where the character made its first appearance. - * @example ##WRONG TYPE## - */ - first_appeared_in_game?: string; - /** - * @description Gender of the character. Available options are: Male, Female, Other - * @example ##WRONG TYPE## - */ - gender?: string; - /** - * @description For use in single item api call for character. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the character. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the character. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Last name of the character. - * @example ##WRONG TYPE## - */ - last_name?: string; - /** - * @description Name of the character. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Real name of the character. - * @example ##WRONG TYPE## - */ - real_name?: string; - /** - * @description URL pointing to the character on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Character.Detail': components['schemas']['Character'] & { - /** - * @description Concepts related to the character. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Enemeis of the character. - * @example ##WRONG TYPE## - */ - enemies?: string; - /** - * @description Franchises related to the character. - * @example ##WRONG TYPE## - */ - franchises?: string; - /** - * @description Friends of the character. - * @example ##WRONG TYPE## - */ - friends?: string; - /** - * @description Games the character has appeared in. - * @example ##WRONG TYPE## - */ - games?: string; - /** - * @description Locations related to the character. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the character. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the character. - * @example ##WRONG TYPE## - */ - people?: string; - }; - /** @description ##ENTER## */ - Chat: { - /** - * @description URL pointing to the chat detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Name of the video streaming channel associated with the chat. - * @example ##WRONG TYPE## - */ - channel_name?: string; - /** - * @description Brief summary of the chat. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description For use in single item api call for chat. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the chat. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the chat. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description chat password. - * @example ##WRONG TYPE## - */ - password?: string; - /** - * @description URL pointing to the chat on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - /** - * @description Title of the chat. - * @example ##WRONG TYPE## - */ - title?: string; - }; - 'Chat.Detail': components['schemas']['Chat'] & unknown; - /** @description ##ENTER## */ - Company: { - /** - * @description Abbreviation of the company. - * @example ##WRONG TYPE## - */ - abbreviation?: string; - /** - * @description List of aliases the company is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the company detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the company was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the company was founded. - * @example ##WRONG TYPE## - */ - date_founded?: string; - /** - * @description Date the company was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the company. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the company. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for company. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the company. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the company. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Street address of the company. - * @example ##WRONG TYPE## - */ - location_address?: string; - /** - * @description City the company resides in. - * @example ##WRONG TYPE## - */ - location_city?: string; - /** - * @description Country the company resides in. - * @example ##WRONG TYPE## - */ - location_country?: string; - /** - * @description State the company resides in. - * @example ##WRONG TYPE## - */ - location_state?: string; - /** - * @description Name of the company. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Phone number of the company. - * @example ##WRONG TYPE## - */ - phone?: string; - /** - * @description URL pointing to the company on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - /** - * @description URL to the company website. - * @example ##WRONG TYPE## - */ - website?: string; - }; - 'Company.Detail': components['schemas']['Company'] & { - /** - * @description Characters related to the company. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Concepts related to the company. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Games the company has developed. - * @example ##WRONG TYPE## - */ - developed_games?: string; - /** - * @description Releases the company has developed. - * @example ##WRONG TYPE## - */ - developer_releases?: string; - /** - * @description Releases the company has distributed. - * @example ##WRONG TYPE## - */ - distributor_releases?: string; - /** - * @description Locations related to the company. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the company. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the company. - * @example ##WRONG TYPE## - */ - people?: string; - /** - * @description Games published by the company. - * @example ##WRONG TYPE## - */ - published_games?: string; - /** - * @description Releases the company has published. - * @example ##WRONG TYPE## - */ - publisher_releases?: string; - }; - /** @description ##ENTER## */ - Concept: { - /** - * @description List of aliases the concept is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the concept detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the concept was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the concept was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the concept. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the concept. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Franchise where the concept made its first appearance. - * @example ##WRONG TYPE## - */ - first_appeared_in_franchise?: string; - /** - * @description Game where the concept made its first appearance. - * @example ##WRONG TYPE## - */ - first_appeared_in_game?: string; - /** - * @description For use in single item api call for concept. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the concept. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the concept. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the concept. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the concept on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Concept.Detail': components['schemas']['Concept'] & { - /** - * @description Characters related to the concept. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Concepts related to the concept. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Franchises related to the concept. - * @example ##WRONG TYPE## - */ - franchises?: string; - /** - * @description Games the concept has appeared in. - * @example ##WRONG TYPE## - */ - games?: string; - /** - * @description Locations related to the concept. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the concept. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the concept. - * @example ##WRONG TYPE## - */ - people?: string; - /** - * @description Other concepts related to the concept. - * @example ##WRONG TYPE## - */ - related_concepts?: string; - }; - /** @description ##ENTER## */ - CurrentLive: { - /** @description ##ENTER## */ - success?: number; - /** @description ##ENTER## */ - video?: { - /** - * @description Thumbnail image of the video - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description URL of the stream (HLS format) - * @example ##WRONG TYPE## - */ - stream?: string; - /** - * @description Title of the live video - * @example ##WRONG TYPE## - */ - title?: string; - } | null; - }; - /** @description ##ENTER## */ - Dlc: { - /** - * @description URL pointing to the dlc detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the dlc was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the dlc was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the dlc. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the dlc. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Game the dlc is for. - * @example ##WRONG TYPE## - */ - game?: string; - /** - * @description For use in single item api call for dlc. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the dlc. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the dlc. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the dlc. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description The dlc's platform. - * @example ##WRONG TYPE## - */ - platform?: string; - /** - * @description Date of the dlc. - * @example ##WRONG TYPE## - */ - release_date?: string; - /** - * @description URL pointing to the dlc on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Dlc.Detail': components['schemas']['Dlc'] & unknown; - /** @description ##ENTER## */ - FieldList: unknown[]; - /** @description ##ENTER## */ - Filter: string; - /** - * @description ##ENTER## - * @enum {string} - */ - Format: 'xml' | 'json' | 'jsonp'; - /** @description ##ENTER## */ - Franchise: { - /** - * @description List of aliases the franchise is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the franchise detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the franchise was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the franchise was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the franchise. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the franchise. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for franchise. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the franchise. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the franchise. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the franchise. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the franchise on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Franchise.Detail': components['schemas']['Franchise'] & { - /** - * @description Characters related to the franchise. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Concepts related to the franchise. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Games the franchise has appeared in. - * @example ##WRONG TYPE## - */ - games?: string; - /** - * @description Locations related to the franchise. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the franchise. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the franchise. - * @example ##WRONG TYPE## - */ - people?: string; - }; - /** @description ##ENTER## */ - Game: { - /** - * @description List of aliases the game is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the game detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the game was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the game was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the game. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the game. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Expected day of release. The month is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_day?: string; - /** - * @description Expected month of release. The month is represented numerically. Combine with 'expected_release_day', 'expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_month?: string; - /** - * @description Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_quarter?: string; - /** - * @description Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'original_release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_year?: string; - /** - * @description For use in single item api call for game. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the game. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the game. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the game. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Number of user reviews of the game on Giant Bomb. - * @example ##WRONG TYPE## - */ - number_of_user_reviews?: string; - /** - * @description Rating of the first release of the game. - * @example ##WRONG TYPE## - */ - original_game_rating?: string; - /** - * @description Date the game was first released. - * @example ##WRONG TYPE## - */ - original_release_date?: string; - /** - * @description The platforms the game exists on. - * @example ##WRONG TYPE## - */ - platforms?: string; - /** - * @description URL pointing to the game on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Game.Detail': components['schemas']['Game'] & { - /** - * @description Characters related to the game. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Concepts related to the game. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Companies who developed the game. - * @example ##WRONG TYPE## - */ - developers?: string; - /** - * @description Game DLCs - * @example ##WRONG TYPE## - */ - dlcs?: string; - /** - * @description Characters that first appeared in the game. - * @example ##WRONG TYPE## - */ - first_appearance_characters?: string; - /** - * @description Concepts that first appeared in the game. - * @example ##WRONG TYPE## - */ - first_appearance_concepts?: string; - /** - * @description Locations that first appeared in the game. - * @example ##WRONG TYPE## - */ - first_appearance_locations?: string; - /** - * @description Objects that first appeared in the game. - * @example ##WRONG TYPE## - */ - first_appearance_objects?: string; - /** - * @description People that were first credited in the game. - * @example ##WRONG TYPE## - */ - first_appearance_people?: string; - /** - * @description Franchises related to the game. - * @example ##WRONG TYPE## - */ - franchises?: string; - /** - * @description Genres that encompass the game. - * @example ##WRONG TYPE## - */ - genres?: string; - /** - * @description List of images associated to the game. - * @example ##WRONG TYPE## - */ - images?: string; - /** - * @description Characters killed in the game. - * @example ##WRONG TYPE## - */ - killed_characters?: string; - /** - * @description Locations related to the game. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the game. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the game. - * @example ##WRONG TYPE## - */ - people?: string; - /** - * @description Companies who published the game. - * @example ##WRONG TYPE## - */ - publishers?: string; - /** - * @description Releases of the game. - * @example ##WRONG TYPE## - */ - releases?: string; - /** - * @description Staff reviews of the game. - * @example ##WRONG TYPE## - */ - reviews?: string; - /** - * @description Other games similar to the game. - * @example ##WRONG TYPE## - */ - similar_games?: string; - /** - * @description Themes that encompass the game. - * @example ##WRONG TYPE## - */ - themes?: string; - /** - * @description Videos associated to the game. - * @example ##WRONG TYPE## - */ - videos?: string; - }; - /** @description ##ENTER## */ - GameId: number; - /** @description ##ENTER## */ - GameRating: { - /** - * @description URL pointing to the game_rating detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description For use in single item api call for game_rating. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the game_rating. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the game_rating. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the game_rating. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Rating board that issues this game_rating. - * @example ##WRONG TYPE## - */ - rating_board?: string; - }; - 'GameRating.Detail': components['schemas']['GameRating'] & unknown; - /** @description ##ENTER## */ - Genre: { - /** - * @description URL pointing to the genre detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the genre was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the genre was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the genre. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the genre. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for genre. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the genre. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the genre. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the genre. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the genre on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Genre.Detail': components['schemas']['Genre'] & unknown; - GetAllSavedTime: { - /** @description ##ENTER## */ - savedTimes?: { - /** - * @description Time/date the progress was saved - * @example ##WRONG TYPE## - */ - savedOn?: string; - /** - * @description Saved time for this video - * @example ##WRONG TYPE## - */ - savedTime?: string; - /** - * @description Id of the video - * @example ##WRONG TYPE## - */ - videoId?: string; - }[]; - /** @description ##ENTER## */ - success?: number; - }; - /** @description ##ENTER## */ - GetSavedTime: { - /** - * @description Saved time of the video or -1 if no time is saved for this user - * @example ##WRONG TYPE## - */ - savedTime?: string; - /** @description ##ENTER## */ - success?: number; - }; - /** @description ##ENTER## */ - Image: { - /** - * @description URL to the icon version of the image. - * @example ##WRONG TYPE## - */ - icon_url?: string; - /** - * @description Name of image tag for filerting images. - * @example ##WRONG TYPE## - */ - image_tag?: string; - /** - * @description URL to the medium size of the image. - * @example ##WRONG TYPE## - */ - medium_url?: string; - /** - * @description URL to the original image. - * @example ##WRONG TYPE## - */ - original_url?: string; - /** - * @description URL to the large screenshot version of the image. - * @example ##WRONG TYPE## - */ - screen_large_url?: string; - /** - * @description URL to the screenshot version of the image. - * @example ##WRONG TYPE## - */ - screen_url?: string; - /** - * @description URL to the small version of the image. - * @example ##WRONG TYPE## - */ - small_url?: string; - /** - * @description URL to the super sized version of the image. - * @example ##WRONG TYPE## - */ - super_url?: string; - /** - * @description URL to the thumb-sized version of the image. - * @example ##WRONG TYPE## - */ - thumb_url?: string; - /** - * @description URL to the tiny version of the image. - * @example ##WRONG TYPE## - */ - tiny_url?: string; - }; - InvalidAPIKey: components['schemas']['Response'] & { - /** @enum {unknown} */ - error?: 'Invalid API Key'; - /** @enum {unknown} */ - results?: never[]; - /** @enum {unknown} */ - status_code?: 100; - }; - /** @description ##ENTER## */ - Limit: number; - /** @description ##ENTER## */ - Location: { - /** - * @description List of aliases the location is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the location detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the location was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the location was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the location. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the location. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Game where the location made its first appearance. - * @example ##WRONG TYPE## - */ - first_appeared_in_game?: string; - /** - * @description For use in single item api call for location. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the location. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the location. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the location. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the location on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Location.Detail': components['schemas']['Location'] & unknown; - /** @description ##ENTER## */ - Object: { - /** - * @description List of aliases the object is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the object detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the object was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the object was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the object. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the object. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Game where the object made its first appearance. - * @example ##WRONG TYPE## - */ - first_appeared_in_game?: string; - /** - * @description For use in single item api call for object. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the object. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the object. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the object. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the object on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Object.Detail': components['schemas']['Object'] & { - /** - * @description Characters related to the object. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Companies related to the object. - * @example ##WRONG TYPE## - */ - companies?: string; - /** - * @description Concepts related to the object. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Franchises related to the object. - * @example ##WRONG TYPE## - */ - franchises?: string; - /** - * @description Games the object has appeared in. - * @example ##WRONG TYPE## - */ - games?: string; - /** - * @description Locations related to the object. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the object. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the object. - * @example ##WRONG TYPE## - */ - people?: string; - }; - /** @description ##ENTER## */ - Offset: number; - /** @description ##ENTER## */ - Page: number; - /** @description ##ENTER## */ - Person: { - /** - * @description List of aliases the person is known by. A \n (newline) seperates each alias. - * @example ##WRONG TYPE## - */ - aliases?: string; - /** - * @description URL pointing to the person detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the person was born. - * @example ##WRONG TYPE## - */ - birth_date?: string; - /** - * @description Country the person resides in. - * @example ##WRONG TYPE## - */ - country?: string; - /** - * @description Date the person was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the person was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Date the person died. - * @example ##WRONG TYPE## - */ - death_date?: string; - /** - * @description Brief summary of the person. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the person. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Game the person was first credited. - * @example ##WRONG TYPE## - */ - first_credited_game?: string; - /** - * @description Gender of the person. Available options are: Male, Female, Other - * @example ##WRONG TYPE## - */ - gender?: string; - /** - * @description For use in single item api call for person. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description City or town the person resides in. - * @example ##WRONG TYPE## - */ - hometown?: string; - /** - * @description Unique ID of the person. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the person. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Name of the person. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the person on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Person.Detail': components['schemas']['Person'] & { - /** - * @description Characters related to the person. - * @example ##WRONG TYPE## - */ - characters?: string; - /** - * @description Concepts related to the person. - * @example ##WRONG TYPE## - */ - concepts?: string; - /** - * @description Franchises related to the person. - * @example ##WRONG TYPE## - */ - franchises?: string; - /** - * @description Games the person has appeared in. - * @example ##WRONG TYPE## - */ - games?: string; - /** - * @description Locations related to the person. - * @example ##WRONG TYPE## - */ - locations?: string; - /** - * @description Objects related to the person. - * @example ##WRONG TYPE## - */ - objects?: string; - /** - * @description People who have worked with the person. - * @example ##WRONG TYPE## - */ - people?: string; - }; - /** @description ##ENTER## */ - Platform: { - /** - * @description Abbreviation of the platform. - * @example ##WRONG TYPE## - */ - abbreviation?: string; - /** - * @description URL pointing to the platform detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Company that created the platform. - * @example ##WRONG TYPE## - */ - company?: string; - /** - * @description Date the platform was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the platform was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the platform. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the platform. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for platform. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the platform. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the platform. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description List of image tags to filter the images endpoint. - * @example ##WRONG TYPE## - */ - image_tags?: string; - /** - * @description Approximate number of sold hardware units. - * @example ##WRONG TYPE## - */ - install_base?: string; - /** - * @description Name of the platform. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Flag indicating whether the platform has online capabilities. - * @example ##WRONG TYPE## - */ - online_support?: string; - /** - * @description Initial price point of the platform. - * @example ##WRONG TYPE## - */ - original_price?: string; - /** - * @description Date of the platform. - * @example ##WRONG TYPE## - */ - release_date?: string; - /** - * @description URL pointing to the platform on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Platform.Detail': components['schemas']['Platform'] & unknown; - /** @description ##ENTER## */ - PlatformId: number; - /** @description ##ENTER## */ - Promo: { - /** - * @description URL pointing to the promo detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the promo was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Brief summary of the promo. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description For use in single item api call for promo. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the promo. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the promo. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description The link that promo points to. - * @example ##WRONG TYPE## - */ - link?: string; - /** - * @description Name of the promo. - * @example ##WRONG TYPE## - */ - name?: string; - resource_type?: components['schemas']['ResourceType'] & unknown; - /** - * @description Author of the promo. - * @example ##WRONG TYPE## - */ - user?: string; - }; - 'Promo.Detail': components['schemas']['Promo'] & unknown; - /** @description ##ENTER## */ - Query: string; - /** @description ##ENTER## */ - RatingBoard: { - /** - * @description URL pointing to the rating_board detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the rating_board was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the rating_board was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the rating_board. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the rating_board. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for rating_board. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the rating_board. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the rating_board. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the rating_board. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the rating_board on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'RatingBoard.Detail': components['schemas']['RatingBoard'] & { - /** - * @description Region the rating_board is responsible for. - * @example ##WRONG TYPE## - */ - region?: string; - }; - /** @description ##ENTER## */ - Region: { - /** - * @description URL pointing to the region detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the region was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the region was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the region. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the region. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description For use in single item api call for region. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the region. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the region. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the region. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the region on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Region.Detail': components['schemas']['Region'] & { - /** - * @description region in the region. - * @example ##WRONG TYPE## - */ - rating_boards?: string; - }; - /** @description ##ENTER## */ - Release: { - /** - * @description URL pointing to the release detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the release was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the release was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the release. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the release. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description "Expected day of release. The day is represented numerically. Combine with 'expected_release_month', 'expected_release_year' and 'expected_release_quarter' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set." - * @example ##WRONG TYPE## - */ - expected_release_day?: string; - /** - * @description "Expected month of release. The month is represented numerically. Combine with 'expected_release_day', expected_release_quarter' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set." - * @example ##WRONG TYPE## - */ - expected_release_month?: string; - /** - * @description Expected quarter of release. The quarter is represented numerically, where 1 = Q1, 2 = Q2, 3 = Q3, and 4 = Q4. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_year' for Giant Bomb's best guess release date of the release. These fields will be empty if the 'release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_quarter?: string; - /** - * @description Expected year of release. Combine with 'expected_release_day', 'expected_release_month' and 'expected_release_quarter' for Giant Bomb's best guess release date of the game. These fields will be empty if the 'release_date' field is set. - * @example ##WRONG TYPE## - */ - expected_release_year?: string; - /** - * @description Game the release is for. - * @example ##WRONG TYPE## - */ - game?: string; - /** - * @description Rating of the release. - * @example ##WRONG TYPE## - */ - game_rating?: string; - /** - * @description For use in single item api call for release. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the release. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the release. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Maximum players - * @example ##WRONG TYPE## - */ - maximum_players?: string; - /** - * @description Minimum players - * @example ##WRONG TYPE## - */ - minimum_players?: string; - /** - * @description Multiplayer features - * @example ##WRONG TYPE## - */ - multiplayer_features?: string; - /** - * @description Name of the release. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description The release's platform. - * @example ##WRONG TYPE## - */ - platform?: string; - /** - * @description The type of product code the release has (ex. UPC/A, ISBN-10, EAN-13). - * @example ##WRONG TYPE## - */ - product_code_type?: string; - /** - * @description The release's product code. - * @example ##WRONG TYPE## - */ - product_code_value?: string; - /** - * @description Region the release is responsible for. - * @example ##WRONG TYPE## - */ - region?: string; - /** - * @description Date of the release. - * @example ##WRONG TYPE## - */ - release_date?: string; - /** - * @description Resolutions available - * @example ##WRONG TYPE## - */ - resolutions?: string; - /** - * @description Singleplayer features - * @example ##WRONG TYPE## - */ - singleplayer_features?: string; - /** - * @description URL pointing to the release on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - /** - * @description Sound systems - * @example ##WRONG TYPE## - */ - sound_systems?: string; - /** - * @description Widescreen support - * @example ##WRONG TYPE## - */ - widescreen_support?: string; - }; - 'Release.Detail': components['schemas']['Release'] & { - /** - * @description Companies who developed the release. - * @example ##WRONG TYPE## - */ - developers?: string; - /** - * @description Companies who published the release. - * @example ##WRONG TYPE## - */ - publishers?: string; - }; - /** - * @description ##ENTER## - * @enum {string} - */ - ResourceType: 'game' | 'franchise' | 'character' | 'concept' | 'object' | 'location' | 'person' | 'company' | 'video'; - /** @description ##ENTER## */ - Response: { - /** @description A text string representing the status_code */ - error?: string; - /** @description The value of the limit filter specified, or 100 if not specified */ - limit?: number; - /** @description The number of results on this page */ - number_of_page_results?: number; - /** @description The number of total results matching the filter conditions specified */ - number_of_total_results?: number; - /** @description The value of the offset filter specified, or 0 if not specified */ - offset?: number; - /** @description Zero or more items that match the filters specified */ - results?: string; - /** @description An integer indicating the result of the request. Acceptable values are:
1:OK
100:Invalid API Key
101:Object Not Found
102:Error in URL Format
103:'jsonp' format requires a 'json_callback' argument
104:Filter Error
105:Subscriber only video is for subscribers only */ - status_code?: number; - }; - /** @description ##ENTER## */ - Review: { - /** - * @description URL pointing to the review detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Brief summary of the review. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the review. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description Name of the Downloadable Content package. - * @example ##WRONG TYPE## - */ - dlc_name?: string; - /** - * @description Game the review is for. - * @example ##WRONG TYPE## - */ - game?: string; - /** - * @description For use in single item api call for review. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the review. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Date the review was published on Giant Bomb. - * @example ##WRONG TYPE## - */ - publish_date?: string; - /** - * @description Release of game for review. - * @example ##WRONG TYPE## - */ - release?: string; - /** - * @description Name of the review's author. - * @example ##WRONG TYPE## - */ - reviewer?: string; - /** - * @description The score given to the game on a scale of 1 to 5. - * @example ##WRONG TYPE## - */ - score?: string; - /** - * @description URL pointing to the review on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Review.Detail': components['schemas']['Review'] & { - /** - * @description Platforms the review appeared in. - * @example ##WRONG TYPE## - */ - platforms?: string; - }; - /** @description ##ENTER## */ - SaveTime: { - /** @description ##ENTER## */ - message?: string; - /** @description ##ENTER## */ - success?: boolean; - }; - Search: ( - | components['schemas']['Game'] - | components['schemas']['Franchise'] - | components['schemas']['Character'] - | components['schemas']['Concept'] - | components['schemas']['Object'] - | components['schemas']['Location'] - | components['schemas']['Person'] - | components['schemas']['Company'] - | components['schemas']['Video'] - ) & { - resource_type?: components['schemas']['ResourceType'] & unknown; - }; - /** @description ##ENTER## */ - Sort: string; - /** @description ##ENTER## */ - SubscriberOnly: boolean; - /** @description ##ENTER## */ - Theme: { - /** - * @description URL pointing to the theme detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description For use in single item api call for theme. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the theme. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Name of the theme. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the theme on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'Theme.Detail': components['schemas']['Theme'] & unknown; - /** @description ##ENTER## */ - TimeToSave: number; - /** @description ##ENTER## */ - Type: { - /** - * @description The name of the type's detail resource. - * @example ##WRONG TYPE## - */ - detail_resource_name?: string; - /** - * @description Unique ID of the type. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description The name of the type's list resource. - * @example ##WRONG TYPE## - */ - list_resource_name?: string; - }; - /** @description ##ENTER## */ - UserReview: { - /** - * @description URL pointing to the user_review detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Date the user_review was added to Giant Bomb. - * @example ##WRONG TYPE## - */ - date_added?: string; - /** - * @description Date the user_review was last updated on Giant Bomb. - * @example ##WRONG TYPE## - */ - date_last_updated?: string; - /** - * @description Brief summary of the user_review. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Description of the user_review. - * @example ##WRONG TYPE## - */ - description?: string; - /** - * @description DLC being reviewed. - * @example ##WRONG TYPE## - */ - dlc?: string; - /** - * @description Game being reviewd. - * @example ##WRONG TYPE## - */ - game?: string; - /** - * @description For use in single item api call for user_review. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the user_review. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Release being reviewed. - * @example ##WRONG TYPE## - */ - release?: string; - /** - * @description Name of the review's author. - * @example ##WRONG TYPE## - */ - reviewer?: string; - /** - * @description The score given to the game on a scale of 1 to 5. - * @example ##WRONG TYPE## - */ - score?: string; - /** - * @description URL pointing to the user_review on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'UserReview.Detail': components['schemas']['UserReview'] & unknown; - /** @description ##ENTER## */ - Video: { - /** - * @description URL pointing to the video detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Related objects to the video. - * @example ##WRONG TYPE## - */ - associations?: string; - /** - * @description Brief summary of the video. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description URL for video embed player. To be inserted into an iFrame.
You can add ?autoplay=true to auto-play.
You can add ?time=x where 'x' is an integer between 0 and the length of the video in seconds to start the video at that point.
You can add ?vol=x where 'x' is a decimal between 0 and 1, .75 for example, to set the starting volume.
The above three parameters may be used together. Example: ?time=45&vol=.5&autoplay=true
See http://www.giantbomb.com/api/video-embed-sample/ for more information on using the embed player. - * @example ##WRONG TYPE## - */ - embed_player?: string; - /** - * @description For use in single item api call for video. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description URL to the HD version of the video. - * @example ##WRONG TYPE## - */ - hd_url?: string; - /** - * @description URL to the High Res version of the video. - * @example ##WRONG TYPE## - */ - high_url?: string; - /** - * @description Unique ID of the video. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the video. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Length (in seconds) of the video. - * @example ##WRONG TYPE## - */ - length_seconds?: string; - /** - * @description URL to the Low Res version of the video. - * @example ##WRONG TYPE## - */ - low_url?: string; - /** - * @description Name of the video. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description Premium status of video. - * @example ##WRONG TYPE## - */ - premium?: string; - /** - * @description Date the video was published on Giant Bomb. - * @example ##WRONG TYPE## - */ - publish_date?: string; - /** - * @description The time where the user left off watching this video - * @example ##WRONG TYPE## - */ - saved_time?: string; - /** - * @description URL pointing to the video on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - /** - * @description The video's filename. - * @example ##WRONG TYPE## - */ - url?: string; - /** - * @description Author of the video. - * @example ##WRONG TYPE## - */ - user?: string; - /** - * @description Video categories - * @example ##WRONG TYPE## - */ - video_categories?: string; - /** - * @description Video show - * @example ##WRONG TYPE## - */ - video_show?: string; - /** - * @description Video category - * @example ##WRONG TYPE## - */ - video_type?: string; - /** - * @description Youtube ID for the video. - * @example ##WRONG TYPE## - */ - youtube_id?: string; - }; - 'Video.Detail': components['schemas']['Video'] & { - /** - * @description Crew of the video. - * @example ##WRONG TYPE## - */ - crew?: string; - /** - * @description Hosts of the video. - * @example ##WRONG TYPE## - */ - hosts?: string; - }; - /** @description ##ENTER## */ - VideoCategory: { - /** - * @description URL pointing to the video_category detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Brief summary of the video_category. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Unique ID of the video_category. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the video_category. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description Name of the video_category. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the video_category on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'VideoCategory.Detail': components['schemas']['VideoCategory'] & unknown; - /** @description ##ENTER## */ - VideoId: number; - /** @description ##ENTER## */ - VideoShow: { - /** - * @description Is this show currently active - * @example ##WRONG TYPE## - */ - active?: string; - /** - * @description URL pointing to the video_show detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Endpoint to retrieve the videos attached to this video_show. - * @example ##WRONG TYPE## - */ - api_videos_url?: string; - /** - * @description Brief summary of the video_show. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Should this show be displayed in navigation menus - * @example ##WRONG TYPE## - */ - display_nav?: string; - /** - * @description For use in single item api call for video_show. - * @example ##WRONG TYPE## - */ - guid?: string; - /** - * @description Unique ID of the video_show. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Main image of the video_show. - * @example ##WRONG TYPE## - */ - image?: string; - /** - * @description The latest episode of a video show. Overrides other sorts when used as a sort field. - * @example ##WRONG TYPE## - */ - latest?: string; - /** - * @description Show logo. - * @example ##WRONG TYPE## - */ - logo?: string; - /** - * @description Editor ordering. - * @example ##WRONG TYPE## - */ - position?: string; - /** - * @description Premium status of video_show. - * @example ##WRONG TYPE## - */ - premium?: string; - /** - * @description URL pointing to the video_show on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - /** - * @description Title of the video_show. - * @example ##WRONG TYPE## - */ - title?: string; - }; - 'VideoShow.Detail': components['schemas']['VideoShow'] & unknown; - /** @description ##ENTER## */ - VideoType: { - /** - * @description URL pointing to the video_type detail resource. - * @example ##WRONG TYPE## - */ - api_detail_url?: string; - /** - * @description Brief summary of the video_type. - * @example ##WRONG TYPE## - */ - deck?: string; - /** - * @description Unique ID of the video_type. - * @example ##WRONG TYPE## - */ - id?: string; - /** - * @description Name of the video_type. - * @example ##WRONG TYPE## - */ - name?: string; - /** - * @description URL pointing to the video_type on Giant Bomb. - * @example ##WRONG TYPE## - */ - site_detail_url?: string; - }; - 'VideoType.Detail': components['schemas']['VideoType'] & unknown; - }; - responses: { - /** @description ##ENTER## */ - InvalidAPIKey: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['InvalidAPIKey']; - 'application/jsonp': components['schemas']['InvalidAPIKey']; - 'application/xml': components['schemas']['InvalidAPIKey']; - }; - }; - }; - parameters: { - FieldList: components['schemas']['FieldList']; - /** @description The result can be filtered by the marked fields in the Fields section below.

Single filter: &filter=field:value
Multiple filters: &filter=field:value,field:value
Date filters: &filter=field:start value|end value (using datetime format) */ - Filter: components['schemas']['Filter']; - /** @description The data format of the response takes either xml, json, or jsonp. */ - Format: components['schemas']['Format']; - /** @description Filter by the ID field on the game resource. */ - Game: components['schemas']['GameId']; - /** @description The number of results to display per page. This value defaults to 100 and can not exceed this number. */ - Limit: components['schemas']['Limit']; - /** @description Return results starting with the object at the offset specified. */ - Offset: components['schemas']['Offset']; - /** @description Page number of search results. */ - Page: components['schemas']['Page']; - /** @description Filters results by platform. The value passed to this filter should be the ID of the platform to filter results by. */ - Platforms: components['schemas']['PlatformId']; - /** @description The search string. */ - Query: components['schemas']['Query']; - /** @description List of resources to filter results. This filter can accept multiple arguments, each delimited with a ",". Available options are:
game
franchise
character
concept
object
location
person
company
video */ - Resources: components['schemas']['ResourceType'][]; - /** @description The result set can be sorted by the marked fields in the Fields section below. Format: &sort=field:direction where direction is either asc or desc. */ - Sort: components['schemas']['Sort']; - /** @description ##ENTER## */ - SubscriberOnly: components['schemas']['SubscriberOnly']; - /** @description The number of seconds into the video the current user is */ - TimeToSave: components['schemas']['TimeToSave']; - /** @description Id of the video */ - VideoId: components['schemas']['VideoId']; - }; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export type operations = Record; diff --git a/api/schemas/MALAPI.ts b/api/schemas/MALAPI.ts deleted file mode 100644 index 7c047f63..00000000 --- a/api/schemas/MALAPI.ts +++ /dev/null @@ -1,6761 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/anime/{id}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeFullById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeCharacters']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/staff': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeStaff']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/episodes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeEpisodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/episodes/{episode}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeEpisodeById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/news': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeNews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/forum': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeForum']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeVideos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/videos/episodes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeVideosEpisodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/pictures': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimePictures']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/statistics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeStatistics']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/moreinfo': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeMoreInfo']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/recommendations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeRecommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/userupdates': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeUserUpdates']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/relations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeRelations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/themes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeThemes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/external': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeExternal']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime/{id}/streaming': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeStreaming']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterFullById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterAnime']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterManga']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}/voices': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterVoiceActors']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters/{id}/pictures': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharacterPictures']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/clubs/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getClubsById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/clubs/{id}/members': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getClubMembers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/clubs/{id}/staff': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getClubStaff']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/clubs/{id}/relations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getClubRelations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/genres/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeGenres']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/genres/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaGenres']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/magazines': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMagazines']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaFullById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaCharacters']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/news': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaNews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/forum': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaTopics']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/pictures': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaPictures']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/statistics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaStatistics']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/moreinfo': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaMoreInfo']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/recommendations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaRecommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/userupdates': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaUserUpdates']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/relations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaRelations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga/{id}/external': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaExternal']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonFullById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonAnime']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}/voices': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonVoices']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonManga']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people/{id}/pictures': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPersonPictures']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/producers/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getProducerById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/producers/{id}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getProducerFullById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/producers/{id}/external': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getProducerExternal']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/random/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRandomAnime']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/random/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRandomManga']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/random/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRandomCharacters']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/random/people': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRandomPeople']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/random/users': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRandomUsers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/recommendations/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRecentAnimeRecommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/recommendations/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRecentMangaRecommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/reviews/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRecentAnimeReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/reviews/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getRecentMangaReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/schedules': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getSchedules']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getAnimeSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getMangaSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/people': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getPeopleSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getCharactersSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUsersSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/userbyid/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserById']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/clubs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getClubsSearch']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/producers': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getProducers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/seasons/now': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getSeasonNow']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/seasons/{year}/{season}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getSeason']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/seasons': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getSeasonsList']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/seasons/upcoming': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getSeasonUpcoming']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/top/anime': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getTopAnime']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/top/manga': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getTopManga']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/top/people': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getTopPeople']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/top/characters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getTopCharacters']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/top/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getTopReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/full': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserFullProfile']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserProfile']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/statistics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserStatistics']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/favorites': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserFavorites']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/userupdates': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserUpdates']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/about': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserAbout']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/history': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserHistory']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/friends': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserFriends']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/animelist': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * @deprecated - * @description User Anime lists have been discontinued since May 1st, 2022. Read more - */ - get: operations['getUserAnimelist']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/mangalist': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * @deprecated - * @description User Manga lists have been discontinued since May 1st, 2022. Read more - */ - get: operations['getUserMangaList']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserReviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/recommendations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserRecommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/clubs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserClubs']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/users/{username}/external': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getUserExternal']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/watch/episodes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getWatchRecentEpisodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/watch/episodes/popular': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getWatchPopularEpisodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/watch/promos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getWatchRecentPromos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/watch/promos/popular': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations['getWatchPopularPromos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** - * @description User's anime list status filter options - * @enum {string} - */ - user_anime_list_status_filter: 'all' | 'watching' | 'completed' | 'onhold' | 'dropped' | 'plantowatch'; - /** - * @description Available Anime order_by properties - * @enum {string} - */ - anime_search_query_orderby: 'mal_id' | 'title' | 'start_date' | 'end_date' | 'episodes' | 'score' | 'scored_by' | 'rank' | 'popularity' | 'members' | 'favorites'; - /** - * @description Available Anime audience ratings

Ratings
  • G - All Ages
  • PG - Children
  • PG-13 - Teens 13 or older
  • R - 17+ (violence & profanity)
  • R+ - Mild Nudity
  • Rx - Hentai
- * @enum {string} - */ - anime_search_query_rating: 'g' | 'pg' | 'pg13' | 'r17' | 'r' | 'rx'; - /** - * @description Available Anime statuses - * @enum {string} - */ - anime_search_query_status: 'airing' | 'complete' | 'upcoming'; - /** - * @description Available Anime types - * @enum {string} - */ - anime_search_query_type: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music' | 'cm' | 'pv' | 'tv_special'; - /** - * @description Available Character order_by properties - * @enum {string} - */ - characters_search_query_orderby: 'mal_id' | 'name' | 'favorites'; - /** - * @description Club Search Query Category - * @enum {string} - */ - club_search_query_category: - | 'anime' - | 'manga' - | 'actors_and_artists' - | 'characters' - | 'cities_and_neighborhoods' - | 'companies' - | 'conventions' - | 'games' - | 'japan' - | 'music' - | 'other' - | 'schools'; - /** - * @description Club Search Query OrderBy - * @enum {string} - */ - club_search_query_orderby: 'mal_id' | 'name' | 'members_count' | 'created'; - /** - * @description Club Search Query Type - * @enum {string} - */ - club_search_query_type: 'public' | 'private' | 'secret'; - /** - * @description Users Search Query Gender. - * @enum {string} - */ - users_search_query_gender: 'any' | 'male' | 'female' | 'nonbinary'; - /** - * @description Filter genres by type - * @enum {string} - */ - genre_query_filter: 'genres' | 'explicit_genres' | 'themes' | 'demographics'; - /** - * @description Order by magazine data - * @enum {string} - */ - magazines_query_orderby: 'mal_id' | 'name' | 'count'; - /** - * @description User's anime list status filter options - * @enum {string} - */ - user_manga_list_status_filter: 'all' | 'reading' | 'completed' | 'onhold' | 'dropped' | 'plantoread'; - /** - * @description Available Manga order_by properties - * @enum {string} - */ - manga_search_query_orderby: - | 'mal_id' - | 'title' - | 'start_date' - | 'end_date' - | 'chapters' - | 'volumes' - | 'score' - | 'scored_by' - | 'rank' - | 'popularity' - | 'members' - | 'favorites'; - /** - * @description Available Manga statuses - * @enum {string} - */ - manga_search_query_status: 'publishing' | 'complete' | 'hiatus' | 'discontinued' | 'upcoming'; - /** - * @description Available Manga types - * @enum {string} - */ - manga_search_query_type: 'manga' | 'novel' | 'lightnovel' | 'oneshot' | 'doujin' | 'manhwa' | 'manhua'; - /** - * @description Available People order_by properties - * @enum {string} - */ - people_search_query_orderby: 'mal_id' | 'name' | 'birthday' | 'favorites'; - /** - * @description Producers Search Query Order By - * @enum {string} - */ - producers_query_orderby: 'mal_id' | 'count' | 'favorites' | 'established'; - /** - * @description Search query sort direction - * @enum {string} - */ - search_query_sort: 'desc' | 'asc'; - /** - * @description Top items filter types - * @enum {string} - */ - top_anime_filter: 'airing' | 'upcoming' | 'bypopularity' | 'favorite'; - /** - * @description Top items filter types - * @enum {string} - */ - top_manga_filter: 'publishing' | 'upcoming' | 'bypopularity' | 'favorite'; - /** - * @description The type of reviews to filter by. Defaults to anime. - * @enum {string} - */ - top_reviews_type_enum: 'anime' | 'manga'; - /** @description Anime Episodes Resource */ - anime_episodes: { - data?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL. This is the URL of the episode's video. If there is no video url, this will be null. */ - url?: string | null; - /** @description Title */ - title?: string; - /** @description Title Japanese */ - title_japanese?: string | null; - /** @description title_romanji */ - title_romanji?: string | null; - /** @description Aired Date ISO8601 */ - aired?: string | null; - /** @description Aggregated episode score (1.00 - 5.00) based on MyAnimeList user voting */ - score?: Record | null; - /** @description Filler episode */ - filler?: boolean; - /** @description Recap episode */ - recap?: boolean; - /** @description Episode discussion forum URL */ - forum_url?: string | null; - }[]; - } & components['schemas']['pagination']; - /** @description Anime News Resource */ - anime_news: components['schemas']['pagination'] & components['schemas']['news']; - /** @description Anime Videos Episodes Resource */ - anime_videos_episodes: { - data?: { - /** @description MyAnimeList ID or Episode Number */ - mal_id?: number; - /** @description Episode Title */ - title?: string; - /** @description Episode Subtitle */ - episode?: string; - /** @description Episode Page URL */ - url?: string; - images?: components['schemas']['common_images']; - }[]; - } & components['schemas']['pagination']; - /** @description Character Pictures */ - character_pictures: { - data?: { - /** @description Default JPG Image Size URL */ - image_url?: string | null; - /** @description Large JPG Image Size URL */ - large_image_url?: string | null; - }[]; - }; - /** @description Club Member */ - club_member: { - data?: { - /** @description User's username */ - username?: string; - /** @description User URL */ - url?: string; - images?: components['schemas']['user_images']; - }[]; - }; - /** @description Manga News Resource */ - manga_news: components['schemas']['pagination'] & components['schemas']['news']; - /** @description Manga Pictures */ - manga_pictures: { - data?: components['schemas']['manga_images'][]; - }; - /** @description Character Pictures */ - person_pictures: { - data?: components['schemas']['people_images'][]; - }; - /** @description Random Resources */ - random: { - data?: (components['schemas']['anime'] | components['schemas']['manga'] | components['schemas']['character'] | components['schemas']['person'])[]; - }; - /** @description Anime resources currently airing */ - schedules: { - data?: components['schemas']['anime'][]; - } & components['schemas']['pagination_plus']; - /** @description User Results */ - users_search: { - data?: { - /** @description MyAnimeList URL */ - url?: string; - /** @description MyAnimeList Username */ - username?: string; - images?: components['schemas']['user_images']; - /** @description Last Online Date ISO8601 */ - last_online?: string; - }[]; - } & components['schemas']['pagination']; - /** @description List of available seasons */ - seasons: { - data?: { - /** @description Year */ - year?: number; - /** @description List of available seasons */ - seasons?: string[]; - }[]; - }; - /** @description Anime & Manga Reviews Resource */ - reviews_collection: { - data?: (components['schemas']['anime_review'] | components['schemas']['manga_review'])[]; - }; - /** @description User Friends */ - user_friends: { - data?: ({ - user?: components['schemas']['user_meta']; - } & { - /** @description Last Online Date ISO8601 format */ - last_online?: string; - /** @description Friends Since Date ISO8601 format */ - friends_since?: string; - })[]; - } & components['schemas']['pagination']; - /** @description User Clubs */ - user_clubs: { - data?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Club Name */ - name?: string; - /** @description Club URL */ - url?: string; - }[]; - } & components['schemas']['pagination']; - /** @description Watch Episodes */ - watch_episodes: { - data?: { - entry?: components['schemas']['anime_meta']; - /** @description Recent Episodes (max 2 listed) */ - episodes?: { - /** @description MyAnimeList ID */ - mal_id?: string; - /** @description MyAnimeList URL */ - url?: string; - /** @description Episode Title */ - title?: string; - /** @description For MyAnimeList Premium Users */ - premium?: boolean; - }[]; - /** @description Region Locked Episode */ - region_locked?: boolean; - }[]; - } & components['schemas']['pagination']; - /** @description Watch Promos */ - watch_promos: components['schemas']['pagination'] & { - data?: { - /** @description Promo Title */ - title?: string; - entry?: components['schemas']['anime_meta']; - trailer?: components['schemas']['trailer']; - }[]; - }; - /** @description Anime Characters Resource */ - anime_characters: { - data?: { - /** @description Character details */ - character?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['character_images']; - /** @description Character Name */ - name?: string; - }; - /** @description Character's Role */ - role?: string; - voice_actors?: { - person?: { - mal_id?: number; - url?: string; - images?: components['schemas']['people_images']; - name?: string; - }; - language?: string; - }[]; - }[]; - }; - /** @description Anime Collection Resource */ - anime_search: { - data?: components['schemas']['anime'][]; - } & components['schemas']['pagination_plus']; - /** @description Anime Episode Resource */ - anime_episode: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Title */ - title?: string; - /** @description Title Japanese */ - title_japanese?: string | null; - /** @description title_romanji */ - title_romanji?: string | null; - /** @description Episode duration in seconds */ - duration?: number | null; - /** @description Aired Date ISO8601 */ - aired?: string | null; - /** @description Filler episode */ - filler?: boolean; - /** @description Recap episode */ - recap?: boolean; - /** @description Episode Synopsis */ - synopsis?: string | null; - }; - /** @description Full anime Resource */ - anime_full: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['anime_images']; - trailer?: components['schemas']['trailer_base']; - /** @description Whether the entry is pending approval on MAL or not */ - approved?: boolean; - /** @description All titles */ - titles?: components['schemas']['title'][]; - /** - * @deprecated - * @description Title - */ - title?: string; - /** - * @deprecated - * @description English Title - */ - title_english?: string | null; - /** - * @deprecated - * @description Japanese Title - */ - title_japanese?: string | null; - /** - * @deprecated - * @description Other Titles - */ - title_synonyms?: string[]; - /** - * @description Anime Type - * @enum {string|null} - */ - type?: 'TV' | 'OVA' | 'Movie' | 'Special' | 'ONA' | 'Music' | null; - /** @description Original Material/Source adapted from */ - source?: string | null; - /** @description Episode count */ - episodes?: number | null; - /** - * @description Airing status - * @enum {string|null} - */ - status?: 'Finished Airing' | 'Currently Airing' | 'Not yet aired' | null; - /** @description Airing boolean */ - airing?: boolean; - aired?: components['schemas']['daterange']; - /** @description Parsed raw duration */ - duration?: string | null; - /** - * @description Anime audience rating - * @enum {string|null} - */ - rating?: 'G - All Ages' | 'PG - Children' | 'PG-13 - Teens 13 or older' | 'R - 17+ (violence & profanity)' | 'R+ - Mild Nudity' | 'Rx - Hentai' | null; - /** - * Format: float - * @description Score - */ - score?: number | null; - /** @description Number of users */ - scored_by?: number | null; - /** @description Ranking */ - rank?: number | null; - /** @description Popularity */ - popularity?: number | null; - /** @description Number of users who have added this entry to their list */ - members?: number | null; - /** @description Number of users who have favorited this entry */ - favorites?: number | null; - /** @description Synopsis */ - synopsis?: string | null; - /** @description Background */ - background?: string | null; - /** - * @description Season - * @enum {string|null} - */ - season?: 'summer' | 'winter' | 'spring' | 'fall' | null; - /** @description Year */ - year?: number | null; - broadcast?: components['schemas']['broadcast']; - producers?: components['schemas']['mal_url'][]; - licensors?: components['schemas']['mal_url'][]; - studios?: components['schemas']['mal_url'][]; - genres?: components['schemas']['mal_url'][]; - explicit_genres?: components['schemas']['mal_url'][]; - themes?: components['schemas']['mal_url'][]; - demographics?: components['schemas']['mal_url'][]; - relations?: { - /** @description Relation type */ - relation?: string; - entry?: components['schemas']['mal_url'][]; - }[]; - theme?: { - openings?: string[]; - endings?: string[]; - }; - external?: { - name?: string; - url?: string; - }[]; - streaming?: { - name?: string; - url?: string; - }[]; - }; - /** @description Anime Relations */ - anime_relations: { - data?: { - /** @description Relation type */ - relation?: string; - entry?: components['schemas']['mal_url'][]; - }[]; - }; - /** @description Anime Resource */ - anime: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['anime_images']; - trailer?: components['schemas']['trailer_base']; - /** @description Whether the entry is pending approval on MAL or not */ - approved?: boolean; - /** @description All titles */ - titles?: components['schemas']['title'][]; - /** - * @deprecated - * @description Title - */ - title?: string; - /** - * @deprecated - * @description English Title - */ - title_english?: string | null; - /** - * @deprecated - * @description Japanese Title - */ - title_japanese?: string | null; - /** - * @deprecated - * @description Other Titles - */ - title_synonyms?: string[]; - /** - * @description Anime Type - * @enum {string|null} - */ - type?: 'TV' | 'OVA' | 'Movie' | 'Special' | 'ONA' | 'Music' | null; - /** @description Original Material/Source adapted from */ - source?: string | null; - /** @description Episode count */ - episodes?: number | null; - /** - * @description Airing status - * @enum {string|null} - */ - status?: 'Finished Airing' | 'Currently Airing' | 'Not yet aired' | null; - /** @description Airing boolean */ - airing?: boolean; - aired?: components['schemas']['daterange']; - /** @description Parsed raw duration */ - duration?: string | null; - /** - * @description Anime audience rating - * @enum {string|null} - */ - rating?: 'G - All Ages' | 'PG - Children' | 'PG-13 - Teens 13 or older' | 'R - 17+ (violence & profanity)' | 'R+ - Mild Nudity' | 'Rx - Hentai' | null; - /** - * Format: float - * @description Score - */ - score?: number | null; - /** @description Number of users */ - scored_by?: number | null; - /** @description Ranking */ - rank?: number | null; - /** @description Popularity */ - popularity?: number | null; - /** @description Number of users who have added this entry to their list */ - members?: number | null; - /** @description Number of users who have favorited this entry */ - favorites?: number | null; - /** @description Synopsis */ - synopsis?: string | null; - /** @description Background */ - background?: string | null; - /** - * @description Season - * @enum {string|null} - */ - season?: 'summer' | 'winter' | 'spring' | 'fall' | null; - /** @description Year */ - year?: number | null; - broadcast?: components['schemas']['broadcast']; - producers?: components['schemas']['mal_url'][]; - licensors?: components['schemas']['mal_url'][]; - studios?: components['schemas']['mal_url'][]; - genres?: components['schemas']['mal_url'][]; - explicit_genres?: components['schemas']['mal_url'][]; - themes?: components['schemas']['mal_url'][]; - demographics?: components['schemas']['mal_url'][]; - }; - /** @description Anime Staff Resource */ - anime_staff: { - data?: { - /** @description Person details */ - person?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['people_images']; - /** @description Name */ - name?: string; - }; - /** @description Staff Positions */ - positions?: string[]; - }[]; - }; - /** @description Anime Statistics Resource */ - anime_statistics: { - data?: { - /** @description Number of users watching the resource */ - watching?: number; - /** @description Number of users who have completed the resource */ - completed?: number; - /** @description Number of users who have put the resource on hold */ - on_hold?: number; - /** @description Number of users who have dropped the resource */ - dropped?: number; - /** @description Number of users who have planned to watch the resource */ - plan_to_watch?: number; - /** @description Total number of users who have the resource added to their lists */ - total?: number; - scores?: { - /** @description Scoring value */ - score?: number; - /** @description Number of votes for this score */ - votes?: number; - /** - * Format: float - * @description Percentage of votes for this score - */ - percentage?: number; - }[]; - }; - }; - /** @description Anime Opening and Ending Themes */ - anime_themes: { - data?: { - openings?: string[]; - endings?: string[]; - }; - }; - /** @description Anime Videos Resource */ - anime_videos: { - data?: { - promo?: { - /** @description Title */ - title?: string; - trailer?: components['schemas']['trailer']; - }[]; - episodes?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Title */ - title?: string; - /** @description Episode */ - episode?: string; - images?: components['schemas']['common_images']; - }[]; - music_videos?: { - /** @description Title */ - title?: string; - video?: components['schemas']['trailer']; - meta?: { - title?: string | null; - author?: string | null; - }; - }[]; - }; - }; - /** @description Character casted in anime */ - character_anime: { - data?: { - /** @description Character's Role */ - role?: string; - anime?: components['schemas']['anime_meta']; - }[]; - }; - /** @description Characters Search Resource */ - characters_search: { - data?: components['schemas']['character'][]; - } & components['schemas']['pagination_plus']; - /** @description Character Resource */ - character_full: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['character_images']; - /** @description Name */ - name?: string; - /** @description Name */ - name_kanji?: string | null; - /** @description Other Names */ - nicknames?: string[]; - /** @description Number of users who have favorited this entry */ - favorites?: number; - /** @description Biography */ - about?: string | null; - anime?: { - /** @description Character's Role */ - role?: string; - anime?: components['schemas']['anime_meta']; - }[]; - manga?: { - /** @description Character's Role */ - role?: string; - manga?: components['schemas']['manga_meta']; - }[]; - voices?: { - /** @description Character's Role */ - language?: string; - person?: components['schemas']['person_meta']; - }[]; - }; - /** @description Character casted in manga */ - character_manga: { - data?: { - /** @description Character's Role */ - role?: string; - manga?: components['schemas']['manga_meta']; - }[]; - }; - /** @description Character Resource */ - character: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['character_images']; - /** @description Name */ - name?: string; - /** @description Name */ - name_kanji?: string | null; - /** @description Other Names */ - nicknames?: string[]; - /** @description Number of users who have favorited this entry */ - favorites?: number; - /** @description Biography */ - about?: string | null; - }; - /** @description Character voice actors */ - character_voice_actors: { - data?: { - /** @description Character's Role */ - language?: string; - person?: components['schemas']['person_meta']; - }[]; - }; - /** @description Clubs Search Resource */ - clubs_search: { - data?: components['schemas']['club'][]; - } & components['schemas']['pagination']; - /** @description Club Relations */ - club_relations: { - data?: { - anime?: components['schemas']['mal_url'][]; - manga?: components['schemas']['mal_url'][]; - characters?: components['schemas']['mal_url'][]; - }; - }; - /** @description Club Resource */ - club: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Club name */ - name?: string; - /** @description Club URL */ - url?: string; - images?: components['schemas']['common_images']; - /** @description Number of club members */ - members?: number; - /** - * @description Club Category - * @enum {string} - */ - category?: - | 'actors & artists' - | 'anime' - | 'characters' - | 'cities & neighborhoods' - | 'companies' - | 'conventions' - | 'games' - | 'japan' - | 'manga' - | 'music' - | 'others' - | 'schools'; - /** @description Date Created ISO8601 */ - created?: string; - /** - * @description Club access - * @enum {string} - */ - access?: 'public' | 'private' | 'secret'; - }; - /** @description Club Staff Resource */ - club_staff: { - data?: { - /** @description User URL */ - url?: string; - /** @description User's username */ - username?: string; - }[]; - }; - /** @description Youtube Details */ - trailer: components['schemas']['trailer_base'] & components['schemas']['trailer_images']; - /** @description Youtube Details */ - trailer_base: { - /** @description YouTube ID */ - youtube_id?: string | null; - /** @description YouTube URL */ - url?: string | null; - /** @description Parsed Embed URL */ - embed_url?: string | null; - }; - /** @description Youtube Images */ - trailer_images: { - images?: { - /** @description Default Image Size URL (120x90) */ - image_url?: string | null; - /** @description Small Image Size URL (640x480) */ - small_image_url?: string | null; - /** @description Medium Image Size URL (320x180) */ - medium_image_url?: string | null; - /** @description Large Image Size URL (480x360) */ - large_image_url?: string | null; - /** @description Maximum Image Size URL (1280x720) */ - maximum_image_url?: string | null; - }; - }; - /** @description Date range */ - daterange: { - /** @description Date ISO8601 */ - from?: string | null; - /** @description Date ISO8601 */ - to?: string | null; - /** @description Date Prop */ - prop?: { - /** @description Date Prop From */ - from?: { - /** @description Day */ - day?: number | null; - /** @description Month */ - month?: number | null; - /** @description Year */ - year?: number | null; - }; - /** @description Date Prop To */ - to?: { - /** @description Day */ - day?: number | null; - /** @description Month */ - month?: number | null; - /** @description Year */ - year?: number | null; - }; - /** @description Raw parsed string */ - string?: string | null; - }; - }; - /** @description Broadcast Details */ - broadcast: { - /** @description Day of the week */ - day?: string | null; - /** @description Time in 24 hour format */ - time?: string | null; - /** @description Timezone (Tz Database format https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) */ - timezone?: string | null; - /** @description Raw parsed broadcast string */ - string?: string | null; - }; - /** @description Parsed URL Data */ - mal_url: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Type of resource */ - type?: string; - /** @description Resource Name/Title */ - name?: string; - /** @description MyAnimeList URL */ - url?: string; - }; - /** @description Parsed URL Data */ - mal_url_2: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Type of resource */ - type?: string; - /** @description Resource Name/Title */ - title?: string; - /** @description MyAnimeList URL */ - url?: string; - }; - /** @description Entry Meta data */ - entry_meta: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Image URL */ - image_url?: string; - /** @description Entry Name/Title */ - name?: string; - }; - /** @description Related resources */ - relation: { - /** @description Relation type */ - relation?: string; - /** @description Related entries */ - entry?: components['schemas']['mal_url'][]; - }; - pagination: { - pagination?: { - last_visible_page?: number; - has_next_page?: boolean; - }; - }; - pagination_plus: { - pagination?: { - last_visible_page?: number; - has_next_page?: boolean; - current_page?: number; - items?: { - count?: number; - total?: number; - per_page?: number; - }; - }; - }; - user_meta: { - /** @description MyAnimeList Username */ - username?: string; - /** @description MyAnimeList Profile URL */ - url?: string; - images?: components['schemas']['user_images']; - }; - /** @description User Meta By ID */ - user_by_id: { - /** @description MyAnimeList URL */ - url?: string; - /** @description MyAnimeList Username */ - username?: string; - }; - user_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - }; - /** @description Available images in WEBP */ - webp?: { - /** @description Image URL WEBP */ - image_url?: string | null; - }; - }; - anime_meta: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['anime_images']; - /** @description Entry title */ - title?: string; - }; - manga_meta: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['manga_images']; - /** @description Entry title */ - title?: string; - }; - character_meta: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['character_images']; - /** @description Entry name */ - name?: string; - }; - person_meta: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['people_images']; - /** @description Entry name */ - name?: string; - }; - anime_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - /** @description Small Image URL JPG */ - small_image_url?: string | null; - /** @description Image URL JPG */ - large_image_url?: string | null; - }; - /** @description Available images in WEBP */ - webp?: { - /** @description Image URL WEBP */ - image_url?: string | null; - /** @description Small Image URL WEBP */ - small_image_url?: string | null; - /** @description Image URL WEBP */ - large_image_url?: string | null; - }; - }; - manga_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - /** @description Small Image URL JPG */ - small_image_url?: string | null; - /** @description Image URL JPG */ - large_image_url?: string | null; - }; - /** @description Available images in WEBP */ - webp?: { - /** @description Image URL WEBP */ - image_url?: string | null; - /** @description Small Image URL WEBP */ - small_image_url?: string | null; - /** @description Image URL WEBP */ - large_image_url?: string | null; - }; - }; - character_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - /** @description Small Image URL JPG */ - small_image_url?: string | null; - }; - /** @description Available images in WEBP */ - webp?: { - /** @description Image URL WEBP */ - image_url?: string | null; - /** @description Small Image URL WEBP */ - small_image_url?: string | null; - }; - }; - people_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - }; - }; - common_images: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG */ - image_url?: string | null; - }; - }; - title: { - /** @description Title type */ - type?: string; - /** @description Title value */ - title?: string; - }; - /** @description External links */ - external_links: { - data?: { - name?: string; - url?: string; - }[]; - }; - /** @description Forum Resource */ - forum: { - data?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Title */ - title?: string; - /** @description Post Date ISO8601 */ - date?: string; - /** @description Author MyAnimeList Username */ - author_username?: string; - /** @description Author Profile URL */ - author_url?: string; - /** @description Comment count */ - comments?: number; - /** @description Last comment details */ - last_comment?: { - /** @description Last comment URL */ - url?: string; - /** @description Author MyAnimeList Username */ - author_username?: string; - /** @description Author Profile URL */ - author_url?: string; - /** @description Last comment date posted ISO8601 */ - date?: string | null; - }; - }[]; - }; - /** @description Genres Collection Resource */ - genres: { - data?: components['schemas']['genre'][]; - }; - /** @description Genre Resource */ - genre: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Genre Name */ - name?: string; - /** @description MyAnimeList URL */ - url?: string; - /** @description Genre's entry count */ - count?: number; - }; - /** @description Magazine Collection Resource */ - magazines: { - data?: components['schemas']['magazine'][]; - } & components['schemas']['pagination']; - /** @description Magazine Resource */ - magazine: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description Magazine Name */ - name?: string; - /** @description MyAnimeList URL */ - url?: string; - /** @description Magazine's manga count */ - count?: number; - }; - /** @description Manga Characters Resource */ - manga_characters: { - data?: { - character?: components['schemas']['character_meta']; - /** @description Character's Role */ - role?: string; - }[]; - }; - /** @description Manga Search Resource */ - manga_search: { - data?: components['schemas']['manga'][]; - } & components['schemas']['pagination_plus']; - /** @description Manga Resource */ - manga_full: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['manga_images']; - /** @description Whether the entry is pending approval on MAL or not */ - approved?: boolean; - /** @description All Titles */ - titles?: components['schemas']['title'][]; - /** - * @deprecated - * @description Title - */ - title?: string; - /** - * @deprecated - * @description English Title - */ - title_english?: string | null; - /** - * @deprecated - * @description Japanese Title - */ - title_japanese?: string | null; - /** - * @deprecated - * @description Other Titles - */ - title_synonyms?: string[]; - /** - * @description Manga Type - * @enum {string|null} - */ - type?: 'Manga' | 'Novel' | 'Light Novel' | 'One-shot' | 'Doujinshi' | 'Manhua' | 'Manhwa' | 'OEL' | null; - /** @description Chapter count */ - chapters?: number | null; - /** @description Volume count */ - volumes?: number | null; - /** - * @description Publishing status - * @enum {string} - */ - status?: 'Finished' | 'Publishing' | 'On Hiatus' | 'Discontinued' | 'Not yet published'; - /** @description Publishing boolean */ - publishing?: boolean; - published?: components['schemas']['daterange']; - /** - * Format: float - * @description Score - */ - score?: number | null; - /** @description Number of users */ - scored_by?: number | null; - /** @description Ranking */ - rank?: number | null; - /** @description Popularity */ - popularity?: number | null; - /** @description Number of users who have added this entry to their list */ - members?: number | null; - /** @description Number of users who have favorited this entry */ - favorites?: number | null; - /** @description Synopsis */ - synopsis?: string | null; - /** @description Background */ - background?: string | null; - authors?: components['schemas']['mal_url'][]; - serializations?: components['schemas']['mal_url'][]; - genres?: components['schemas']['mal_url'][]; - explicit_genres?: components['schemas']['mal_url'][]; - themes?: components['schemas']['mal_url'][]; - demographics?: components['schemas']['mal_url'][]; - relations?: { - /** @description Relation type */ - relation?: string; - entry?: components['schemas']['mal_url'][]; - }[]; - external?: { - name?: string; - url?: string; - }[]; - }; - /** @description Manga Resource */ - manga: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['manga_images']; - /** @description Whether the entry is pending approval on MAL or not */ - approved?: boolean; - /** @description All Titles */ - titles?: components['schemas']['title'][]; - /** - * @deprecated - * @description Title - */ - title?: string; - /** - * @deprecated - * @description English Title - */ - title_english?: string | null; - /** - * @deprecated - * @description Japanese Title - */ - title_japanese?: string | null; - /** - * @description Manga Type - * @enum {string|null} - */ - type?: 'Manga' | 'Novel' | 'Light Novel' | 'One-shot' | 'Doujinshi' | 'Manhua' | 'Manhwa' | 'OEL' | null; - /** @description Chapter count */ - chapters?: number | null; - /** @description Volume count */ - volumes?: number | null; - /** - * @description Publishing status - * @enum {string} - */ - status?: 'Finished' | 'Publishing' | 'On Hiatus' | 'Discontinued' | 'Not yet published'; - /** @description Publishing boolean */ - publishing?: boolean; - published?: components['schemas']['daterange']; - /** - * Format: float - * @description Score - */ - score?: number | null; - /** @description Number of users */ - scored_by?: number | null; - /** @description Ranking */ - rank?: number | null; - /** @description Popularity */ - popularity?: number | null; - /** @description Number of users who have added this entry to their list */ - members?: number | null; - /** @description Number of users who have favorited this entry */ - favorites?: number | null; - /** @description Synopsis */ - synopsis?: string | null; - /** @description Background */ - background?: string | null; - authors?: components['schemas']['mal_url'][]; - serializations?: components['schemas']['mal_url'][]; - genres?: components['schemas']['mal_url'][]; - explicit_genres?: components['schemas']['mal_url'][]; - themes?: components['schemas']['mal_url'][]; - demographics?: components['schemas']['mal_url'][]; - }; - /** @description Manga Statistics Resource */ - manga_statistics: { - data?: { - /** @description Number of users reading the resource */ - reading?: number; - /** @description Number of users who have completed the resource */ - completed?: number; - /** @description Number of users who have put the resource on hold */ - on_hold?: number; - /** @description Number of users who have dropped the resource */ - dropped?: number; - /** @description Number of users who have planned to read the resource */ - plan_to_read?: number; - /** @description Total number of users who have the resource added to their lists */ - total?: number; - scores?: { - /** @description Scoring value */ - score?: number; - /** @description Number of votes for this score */ - votes?: number; - /** - * Format: float - * @description Percentage of votes for this score - */ - percentage?: number; - }[]; - }; - }; - /** @description More Info Resource */ - moreinfo: { - data?: { - /** @description Additional information on the entry */ - moreinfo?: string | null; - }; - }; - news: { - data?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Title */ - title?: string; - /** @description Post Date ISO8601 */ - date?: string; - /** @description Author MyAnimeList Username */ - author_username?: string; - /** @description Author Profile URL */ - author_url?: string; - /** @description Forum topic URL */ - forum_url?: string; - images?: components['schemas']['common_images']; - /** @description Comment count */ - comments?: number; - /** @description Excerpt */ - excerpt?: string; - }[]; - }; - /** @description Person anime staff positions */ - person_anime: { - data?: { - /** @description Person's position */ - position?: string; - anime?: components['schemas']['anime_meta']; - }[]; - }; - /** @description People Search */ - people_search: { - data?: components['schemas']['person'][]; - } & components['schemas']['pagination_plus']; - /** @description Person Resource */ - person_full: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Person's website URL */ - website_url?: string | null; - images?: components['schemas']['people_images']; - /** @description Name */ - name?: string; - /** @description Given Name */ - given_name?: string | null; - /** @description Family Name */ - family_name?: string | null; - /** @description Other Names */ - alternate_names?: string[]; - /** @description Birthday Date ISO8601 */ - birthday?: string | null; - /** @description Number of users who have favorited this entry */ - favorites?: number; - /** @description Biography */ - about?: string | null; - anime?: { - /** @description Person's position */ - position?: string; - anime?: components['schemas']['anime_meta']; - }[]; - manga?: { - /** @description Person's position */ - position?: string; - manga?: components['schemas']['manga_meta']; - }[]; - voices?: { - /** @description Person's Character's role in the anime */ - role?: string; - anime?: components['schemas']['anime_meta']; - character?: components['schemas']['character_meta']; - }[]; - }; - /** @description Person's mangaography */ - person_manga: { - data?: { - /** @description Person's position */ - position?: string; - manga?: components['schemas']['manga_meta']; - }[]; - }; - /** @description Person Resource */ - person: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description Person's website URL */ - website_url?: string | null; - images?: components['schemas']['people_images']; - /** @description Name */ - name?: string; - /** @description Given Name */ - given_name?: string | null; - /** @description Family Name */ - family_name?: string | null; - /** @description Other Names */ - alternate_names?: string[]; - /** @description Birthday Date ISO8601 */ - birthday?: string | null; - /** @description Number of users who have favorited this entry */ - favorites?: number; - /** @description Biography */ - about?: string | null; - }; - /** @description Person's voice acting roles */ - person_voice_acting_roles: { - data?: { - /** @description Person's Character's role in the anime */ - role?: string; - anime?: components['schemas']['anime_meta']; - character?: components['schemas']['character_meta']; - }[]; - }; - /** @description Pictures Resource */ - pictures: { - data?: { - images?: components['schemas']['anime_images']; - }[]; - }; - /** @description Pictures Resource */ - pictures_variants: { - data?: { - images?: components['schemas']['common_images']; - }[]; - }; - /** @description Producers Collection Resource */ - producers: { - data?: components['schemas']['producer'][]; - } & components['schemas']['pagination']; - /** @description Producers Resource */ - producer_full: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description All titles */ - titles?: components['schemas']['title'][]; - images?: components['schemas']['common_images']; - /** @description Producers's member favorites count */ - favorites?: number; - /** @description Producers's anime count */ - count?: number; - /** @description Established Date ISO8601 */ - established?: string | null; - /** @description About the Producer */ - about?: string | null; - external?: { - name?: string; - url?: string; - }[]; - }; - /** @description Producers Resource */ - producer: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList URL */ - url?: string; - /** @description All titles */ - titles?: components['schemas']['title'][]; - images?: components['schemas']['common_images']; - /** @description Producers's member favorites count */ - favorites?: number; - /** @description Producers's anime count */ - count?: number; - /** @description Established Date ISO8601 */ - established?: string | null; - /** @description About the Producer */ - about?: string | null; - }; - user_about: { - data?: { - /** @description User About. NOTE: About information is customizable by users through BBCode on MyAnimeList. This means users can add multimedia content, different text sizes, etc. Due to this freeform, Jikan returns parsed HTML. Validate on your end! */ - about?: string | null; - }[]; - }; - user_favorites: { - /** @description Favorite Anime */ - anime?: ({ - type?: string; - start_year?: number; - } & components['schemas']['anime_meta'])[]; - /** @description Favorite Manga */ - manga?: ({ - type?: string; - start_year?: number; - } & components['schemas']['manga_meta'])[]; - /** @description Favorite Characters */ - characters?: (components['schemas']['character_meta'] & components['schemas']['mal_url_2'])[]; - /** @description Favorite People */ - people?: components['schemas']['character_meta'][]; - }; - /** @description Transform the resource into an array. */ - user_profile_full: { - /** @description MyAnimeList ID */ - mal_id?: number | null; - /** @description MyAnimeList Username */ - username?: string; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['user_images']; - /** @description Last Online Date ISO8601 */ - last_online?: string | null; - /** @description User Gender */ - gender?: string | null; - /** @description Birthday Date ISO8601 */ - birthday?: string | null; - /** @description Location */ - location?: string | null; - /** @description Joined Date ISO8601 */ - joined?: string | null; - statistics?: { - /** @description Anime Statistics */ - anime?: { - /** - * Format: float - * @description Number of days spent watching Anime - */ - days_watched?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Anime Watching */ - watching?: number; - /** @description Anime Completed */ - completed?: number; - /** @description Anime On-Hold */ - on_hold?: number; - /** @description Anime Dropped */ - dropped?: number; - /** @description Anime Planned to Watch */ - plan_to_watch?: number; - /** @description Total Anime entries on User list */ - total_entries?: number; - /** @description Anime re-watched */ - rewatched?: number; - /** @description Number of Anime Episodes Watched */ - episodes_watched?: number; - }; - /** @description Manga Statistics */ - manga?: { - /** - * Format: float - * @description Number of days spent reading Manga - */ - days_read?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Manga Reading */ - reading?: number; - /** @description Manga Completed */ - completed?: number; - /** @description Manga On-Hold */ - on_hold?: number; - /** @description Manga Dropped */ - dropped?: number; - /** @description Manga Planned to Read */ - plan_to_read?: number; - /** @description Total Manga entries on User list */ - total_entries?: number; - /** @description Manga re-read */ - reread?: number; - /** @description Number of Manga Chapters Read */ - chapters_read?: number; - /** @description Number of Manga Volumes Read */ - volumes_read?: number; - }; - }; - external?: { - name?: string; - url?: string; - }[]; - }; - user_history: { - data?: components['schemas']['history'][]; - }; - /** @description Transform the resource into an array. */ - history: { - entry?: components['schemas']['mal_url']; - /** @description Number of episodes/chapters watched/read */ - increment?: number; - /** @description Date ISO8601 */ - date?: string; - }; - user_updates: { - data?: { - /** @description Last updated Anime */ - anime?: ({ - entry?: components['schemas']['anime_meta']; - } & { - score?: number | null; - status?: string; - episodes_seen?: number | null; - episodes_total?: number | null; - /** @description ISO8601 format */ - date?: string; - })[]; - /** @description Last updated Manga */ - manga?: ({ - entry?: components['schemas']['manga_meta']; - } & { - score?: number | null; - status?: string; - chapters_read?: number | null; - chapters_total?: number | null; - volumes_read?: number | null; - volumes_total?: number | null; - /** @description ISO8601 format */ - date?: string; - })[]; - }; - }; - user_profile: { - /** @description MyAnimeList ID */ - mal_id?: number | null; - /** @description MyAnimeList Username */ - username?: string; - /** @description MyAnimeList URL */ - url?: string; - images?: components['schemas']['user_images']; - /** @description Last Online Date ISO8601 */ - last_online?: string | null; - /** @description User Gender */ - gender?: string | null; - /** @description Birthday Date ISO8601 */ - birthday?: string | null; - /** @description Location */ - location?: string | null; - /** @description Joined Date ISO8601 */ - joined?: string | null; - }; - /** @description Transform the resource into an array. */ - users_temp: { - data?: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList Username */ - username?: string; - /** @description MyAnimeList URL */ - url?: string; - /** @description Images */ - images?: { - /** @description Available images in JPG */ - jpg?: { - /** @description Image URL JPG (225x335) */ - image_url?: string; - }; - /** @description Available images in WEBP */ - webp?: { - /** @description Image URL WEBP (225x335) */ - image_url?: string; - }; - }; - /** @description Last Online Date ISO8601 */ - last_online?: string; - /** @description User Gender */ - gender?: string; - /** @description Birthday Date ISO8601 */ - birthday?: string; - /** @description Location */ - location?: string; - /** @description Joined Date ISO8601 */ - joined?: string; - /** @description Anime Stats */ - anime_stats?: { - /** - * Format: float - * @description Number of days spent watching Anime - */ - days_watched?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Anime Watching */ - watching?: number; - /** @description Anime Completed */ - completed?: number; - /** @description Anime On-Hold */ - on_hold?: number; - /** @description Anime Dropped */ - dropped?: number; - /** @description Anime Planned to Watch */ - plan_to_watch?: number; - /** @description Total Anime entries on User list */ - total_entries?: number; - /** @description Anime re-watched */ - rewatched?: number; - /** @description Number of Anime Episodes Watched */ - episodes_watched?: number; - }; - /** @description Manga Stats */ - manga_stats?: { - /** - * Format: float - * @description Number of days spent reading Manga - */ - days_read?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Manga Reading */ - reading?: number; - /** @description Manga Completed */ - completed?: number; - /** @description Manga On-Hold */ - on_hold?: number; - /** @description Manga Dropped */ - dropped?: number; - /** @description Manga Planned to Read */ - plan_to_read?: number; - /** @description Total Manga entries on User list */ - total_entries?: number; - /** @description Manga re-read */ - reread?: number; - /** @description Number of Manga Chapters Read */ - chapters_read?: number; - /** @description Number of Manga Volumes Read */ - volumes_read?: number; - }; - /** @description Favorite entries */ - favorites?: { - /** @description Favorite Anime */ - anime?: components['schemas']['entry_meta'][]; - /** @description Favorite Manga */ - manga?: components['schemas']['entry_meta'][]; - /** @description Favorite Characters */ - characters?: components['schemas']['entry_meta'][]; - /** @description Favorite People */ - people?: components['schemas']['entry_meta'][]; - }; - /** @description User About. NOTE: About information is customizable by users through BBCode on MyAnimeList. This means users can add multimedia content, different text sizes, etc. Due to this freeform, Jikan returns parsed HTML. Validate on your end! */ - about?: string; - }[]; - }; - user_statistics: { - data?: { - /** @description Anime Statistics */ - anime?: { - /** - * Format: float - * @description Number of days spent watching Anime - */ - days_watched?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Anime Watching */ - watching?: number; - /** @description Anime Completed */ - completed?: number; - /** @description Anime On-Hold */ - on_hold?: number; - /** @description Anime Dropped */ - dropped?: number; - /** @description Anime Planned to Watch */ - plan_to_watch?: number; - /** @description Total Anime entries on User list */ - total_entries?: number; - /** @description Anime re-watched */ - rewatched?: number; - /** @description Number of Anime Episodes Watched */ - episodes_watched?: number; - }; - /** @description Manga Statistics */ - manga?: { - /** - * Format: float - * @description Number of days spent reading Manga - */ - days_read?: number; - /** - * Format: float - * @description Mean Score - */ - mean_score?: number; - /** @description Manga Reading */ - reading?: number; - /** @description Manga Completed */ - completed?: number; - /** @description Manga On-Hold */ - on_hold?: number; - /** @description Manga Dropped */ - dropped?: number; - /** @description Manga Planned to Read */ - plan_to_read?: number; - /** @description Total Manga entries on User list */ - total_entries?: number; - /** @description Manga re-read */ - reread?: number; - /** @description Number of Manga Chapters Read */ - chapters_read?: number; - /** @description Number of Manga Volumes Read */ - volumes_read?: number; - }; - }; - }; - /** @description Recommendations */ - recommendations: { - data?: { - /** @description MAL IDs of recommendations is both of the MAL ID's with a `-` delimiter */ - mal_id?: string; - /** @description Array of 2 entries that are being recommended to each other */ - entry?: (components['schemas']['anime_meta'] | components['schemas']['manga_meta'])[]; - /** @description Recommendation context provided by the user */ - content?: string; - user?: components['schemas']['user_by_id']; - }[]; - } & components['schemas']['pagination']; - /** @description Entry Recommendations Resource */ - entry_recommendations: { - data?: { - entry?: components['schemas']['anime_meta'] | components['schemas']['manga_meta']; - }[]; - }; - manga_review: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList review URL */ - url?: string; - /** @description Entry type */ - type?: string; - /** @description User reaction count on the review */ - reactions?: { - /** @description Overall reaction count */ - overall?: number; - /** @description Nice reaction count */ - nice?: number; - /** @description Love it reaction count */ - love_it?: number; - /** @description Funny reaction count */ - funny?: number; - /** @description Confusing reaction count */ - confusing?: number; - /** @description Informative reaction count */ - informative?: number; - /** @description Well written reaction count */ - well_written?: number; - /** @description Creative reaction count */ - creative?: number; - }; - /** @description Review created date ISO8601 */ - date?: string; - /** @description Review content */ - review?: string; - /** @description Number of user votes on the Review */ - score?: number; - /** @description Review tags */ - tags?: string[]; - /** @description The review contains spoiler */ - is_spoiler?: boolean; - /** @description The review was made before the entry was completed */ - is_preliminary?: boolean; - }; - anime_review: { - /** @description MyAnimeList ID */ - mal_id?: number; - /** @description MyAnimeList review URL */ - url?: string; - /** @description Entry type */ - type?: string; - /** @description User reaction count on the review */ - reactions?: { - /** @description Overall reaction count */ - overall?: number; - /** @description Nice reaction count */ - nice?: number; - /** @description Love it reaction count */ - love_it?: number; - /** @description Funny reaction count */ - funny?: number; - /** @description Confusing reaction count */ - confusing?: number; - /** @description Informative reaction count */ - informative?: number; - /** @description Well written reaction count */ - well_written?: number; - /** @description Creative reaction count */ - creative?: number; - }; - /** @description Review created date ISO8601 */ - date?: string; - /** @description Review content */ - review?: string; - /** @description Number of user votes on the Review */ - score?: number; - /** @description Review tags */ - tags?: string[]; - /** @description The review contains spoiler */ - is_spoiler?: boolean; - /** @description The review was made before the entry was completed */ - is_preliminary?: boolean; - /** @description Number of episodes watched */ - episodes_watched?: number; - }; - /** @description Anime Reviews Resource */ - anime_reviews: { - data?: ({ - user?: components['schemas']['user_meta']; - } & components['schemas']['anime_review'])[]; - } & components['schemas']['pagination']; - /** @description Manga Reviews Resource */ - manga_reviews: { - data?: ({ - user?: components['schemas']['user_meta']; - } & components['schemas']['manga_review'])[]; - } & components['schemas']['pagination']; - /** @description Streaming links */ - streaming_links: { - data?: { - name?: string; - url?: string; - }[]; - }; - /** @description Anime User Updates Resource */ - anime_userupdates: { - data?: { - user?: components['schemas']['user_meta']; - /** @description User Score */ - score?: number | null; - /** @description User list status */ - status?: string; - /** @description Number of episodes seen */ - episodes_seen?: number | null; - /** @description Total number of episodes */ - episodes_total?: number | null; - /** @description Last updated date ISO8601 */ - date?: string; - }[]; - } & components['schemas']['pagination']; - /** @description Manga User Updates Resource */ - manga_userupdates: { - data?: { - user?: components['schemas']['user_meta']; - /** @description User Score */ - score?: number | null; - /** @description User list status */ - status?: string; - /** @description Number of volumes read */ - volumes_read?: number; - /** @description Total number of volumes */ - volumes_total?: number; - /** @description Number of chapters read */ - chapters_read?: number; - /** @description Total number of chapters */ - chapters_total?: number; - /** @description Last updated date ISO8601 */ - date?: string; - }[]; - } & components['schemas']['pagination']; - }; - responses: { - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - parameters: { - /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ - continuing: boolean; - /** @description This is a flag. When supplied it will include entries with the Kids genres in specific endpoints that filter them out by default. You do not need to pass a value to it. e.g usage: `?kids` */ - kids: boolean; - limit: number; - page: number; - /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ - preliminary: boolean; - /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ - sfw: boolean; - /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ - spoilers: boolean; - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved: boolean; - }; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - getAnimeFullById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns complete anime resource data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['anime_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['anime']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeCharacters: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime characters resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_characters']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeStaff: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime staff resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_staff']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeEpisodes: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of anime episodes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_episodes']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeEpisodeById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - episode: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a single anime episode resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['anime_episode']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeNews: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of news articles related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_news']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeForum: { - parameters: { - query?: { - /** @description Filter topics */ - filter?: 'all' | 'episode' | 'other'; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of forum topics related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['forum']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeVideos: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns videos related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_videos']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeVideosEpisodes: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns episode videos related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_videos_episodes']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimePictures: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns pictures related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['pictures_variants']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeStatistics: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime statistics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_statistics']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeMoreInfo: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime statistics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['moreinfo']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeRecommendations: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime recommendations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['entry_recommendations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeUserUpdates: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of users who have added/updated/removed the entry on their list */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_userupdates']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ - preliminary?: components['parameters']['preliminary']; - /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ - spoilers?: components['parameters']['spoilers']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_reviews']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeRelations: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime relations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['relation'][]; - }; - }; - }; - }; - }; - getAnimeThemes: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime themes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_themes']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeExternal: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime external links */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['external_links']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeStreaming: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime streaming links */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['external_links']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterFullById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns complete character resource data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['character_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns character resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['character']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterAnime: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime that character is in */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['character_anime']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterManga: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga that character is in */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['character_manga']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterVoiceActors: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns the character's voice actors */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['character_voice_actors']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharacterPictures: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns pictures related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['character_pictures']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getClubsById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Club Resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['club']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getClubMembers: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Club Members Resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['pagination'] & components['schemas']['club_member']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getClubStaff: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Club Staff */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['club_staff']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getClubRelations: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Club Relations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['club_relations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeGenres: { - parameters: { - query?: { - filter?: components['schemas']['genre_query_filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns entry genres, explicit_genres, themes and demographics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['genres']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaGenres: { - parameters: { - query?: { - filter?: components['schemas']['genre_query_filter']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns entry genres, explicit_genres, themes and demographics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['genres']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMagazines: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - order_by?: components['schemas']['magazines_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns magazines collection */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['magazines']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaFullById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns complete manga resource data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['manga_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns pictures related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['manga']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaCharacters: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga characters resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_characters']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaNews: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of manga news topics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_news']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaTopics: { - parameters: { - query?: { - /** @description Filter topics */ - filter?: 'all' | 'episode' | 'other'; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of manga forum topics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['forum']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaPictures: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of manga pictures */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_pictures']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaStatistics: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns anime statistics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_statistics']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaMoreInfo: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga moreinfo */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['moreinfo']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaRecommendations: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga recommendations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['entry_recommendations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaUserUpdates: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga user updates */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_userupdates']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ - preliminary?: components['parameters']['preliminary']; - /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ - spoilers?: components['parameters']['spoilers']; - }; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_reviews']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaRelations: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga relations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['relation'][]; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaExternal: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns manga external links */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['external_links']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonFullById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns complete character resource data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['person_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns pictures related to the entry */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['person']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonAnime: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns person's anime staff positions */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['person_anime']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonVoices: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns person's voice acting roles */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['person_voice_acting_roles']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonManga: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns person's published manga works */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['person_manga']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPersonPictures: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a list of pictures of the person */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['person_pictures']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getProducerById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns producer resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['producer']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getProducerFullById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns producer resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['producer_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getProducerExternal: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns producer's external links */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['external_links']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRandomAnime: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a random anime resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['anime']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRandomManga: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a random manga resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['manga']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRandomCharacters: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a random character resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['character']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRandomPeople: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a random person resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['person']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRandomUsers: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns a random user profile resource */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['user_profile']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRecentAnimeRecommendations: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns recent anime recommendations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['recommendations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRecentMangaRecommendations: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns recent manga recommendations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['recommendations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRecentAnimeReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ - preliminary?: components['parameters']['preliminary']; - /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ - spoilers?: components['parameters']['spoilers']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns recent anime reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getRecentMangaReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - /** @description Any reviews left during an ongoing anime/manga, those reviews are tagged as preliminary. NOTE: Preliminary reviews are not returned by default so if the entry is airing/publishing you need to add this otherwise you will get an empty list. e.g usage: `?preliminary=true` */ - preliminary?: components['parameters']['preliminary']; - /** @description Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true` */ - spoilers?: components['parameters']['spoilers']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns recent manga reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getSchedules: { - parameters: { - query?: { - /** @description Filter by day */ - filter?: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' | 'unknown' | 'other'; - /** @description When supplied, it will filter entries with the `Kids` Genre Demographic. When supplied as `kids=true`, it will return only Kid entries and when supplied as `kids=false`, it will filter out any Kid entries. Defaults to `false`. */ - kids?: 'true' | 'false'; - /** @description 'Safe For Work'. When supplied, it will filter entries with the `Hentai` Genre. When supplied as `sfw=true`, it will return only SFW entries and when supplied as `sfw=false`, it will filter out any Hentai entries. Defaults to `false`. */ - sfw?: 'true' | 'false'; - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns weekly schedule */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['schedules']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getAnimeSearch: { - parameters: { - query?: { - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - type?: components['schemas']['anime_search_query_type']; - score?: number; - /** @description Set a minimum score for results. */ - min_score?: number; - /** @description Set a maximum score for results */ - max_score?: number; - status?: components['schemas']['anime_search_query_status']; - rating?: components['schemas']['anime_search_query_rating']; - /** @description Filter out Adult entries */ - sfw?: boolean; - /** @description Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - genres?: string; - /** @description Exclude genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - genres_exclude?: string; - order_by?: components['schemas']['anime_search_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - /** @description Filter by producer(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - producers?: string; - /** @description Filter by starting date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ - start_date?: string; - /** @description Filter by ending date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ - end_date?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for anime */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getMangaSearch: { - parameters: { - query?: { - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - type?: components['schemas']['manga_search_query_type']; - score?: number; - /** @description Set a minimum score for results. */ - min_score?: number; - /** @description Set a maximum score for results */ - max_score?: number; - status?: components['schemas']['manga_search_query_status']; - /** @description Filter out Adult entries */ - sfw?: boolean; - /** @description Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - genres?: string; - /** @description Exclude genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - genres_exclude?: string; - order_by?: components['schemas']['manga_search_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - /** @description Filter by magazine(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3 */ - magazines?: string; - /** @description Filter by starting date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ - start_date?: string; - /** @description Filter by ending date. Format: YYYY-MM-DD. e.g `2022`, `2005-05`, `2005-01-01` */ - end_date?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for manga */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getPeopleSearch: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - order_by?: components['schemas']['people_search_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for people */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['people_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getCharactersSearch: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - order_by?: components['schemas']['characters_search_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for characters */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['characters_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUsersSearch: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - gender?: components['schemas']['users_search_query_gender']; - location?: string; - maxAge?: number; - minAge?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['users_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserById: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns username by ID search */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['user_by_id']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getClubsSearch: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - type?: components['schemas']['club_search_query_type']; - category?: components['schemas']['club_search_query_category']; - order_by?: components['schemas']['club_search_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns search results for clubs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['clubs_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getProducers: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - q?: string; - order_by?: components['schemas']['producers_query_orderby']; - sort?: components['schemas']['search_query_sort']; - /** @description Return entries starting with the given letter */ - letter?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns producers collection */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['producers']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getSeasonNow: { - parameters: { - query?: { - /** @description Entry types */ - filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; - /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ - sfw?: components['parameters']['sfw']; - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ - continuing?: components['parameters']['continuing']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns current seasonal anime */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getSeason: { - parameters: { - query?: { - /** @description Entry types */ - filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; - /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ - sfw?: components['parameters']['sfw']; - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ - continuing?: components['parameters']['continuing']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path: { - year: number; - season: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns seasonal anime */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getSeasonsList: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns available list of seasons */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['seasons']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getSeasonUpcoming: { - parameters: { - query?: { - /** @description Entry types */ - filter?: 'tv' | 'movie' | 'ova' | 'special' | 'ona' | 'music'; - /** @description 'Safe For Work'. This is a flag. When supplied it will filter out entries according to the SFW Policy. You do not need to pass a value to it. e.g usage: `?sfw` */ - sfw?: components['parameters']['sfw']; - /** @description This is a flag. When supplied it will include entries which are unapproved. Unapproved entries on MyAnimeList are those that are user submitted and have not yet been approved by MAL to show up on other pages. They will have their own specifc pages and are often removed resulting in a 404 error. You do not need to pass a value to it. e.g usage: `?unapproved` */ - unapproved?: components['parameters']['unapproved']; - /** @description This is a flag. When supplied it will include entries which are continuing from previous seasons. MAL includes these items on the seasons view in the ″TV (continuing)″ section. (Example: https://myanimelist.net/anime/season/2024/winter)
Example usage: `?continuing` */ - continuing?: components['parameters']['continuing']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns upcoming season's anime */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getTopAnime: { - parameters: { - query?: { - type?: components['schemas']['anime_search_query_type']; - filter?: components['schemas']['top_anime_filter']; - rating?: components['schemas']['anime_search_query_rating']; - /** @description Filter out Adult entries */ - sfw?: boolean; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns top anime */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['anime_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getTopManga: { - parameters: { - query?: { - type?: components['schemas']['manga_search_query_type']; - filter?: components['schemas']['top_manga_filter']; - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns top manga */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['manga_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getTopPeople: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns top people */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['people_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getTopCharacters: { - parameters: { - query?: { - page?: components['parameters']['page']; - limit?: components['parameters']['limit']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns top characters */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['characters_search']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getTopReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - type?: components['schemas']['top_reviews_type_enum']; - /** @description Whether the results include preliminary reviews or not. Defaults to true. */ - preliminary?: boolean; - /** @description Whether the results include reviews with spoilers or not. Defaults to true. */ - spoilers?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns top reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: { - data?: ( - | ({ - user?: components['schemas']['user_meta']; - } & { - anime?: components['schemas']['anime_meta']; - } & components['schemas']['anime_review']) - | ({ - user?: components['schemas']['user_meta']; - } & { - manga?: components['schemas']['manga_meta']; - } & components['schemas']['manga_review']) - )[]; - } & components['schemas']['pagination']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserFullProfile: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns complete user resource data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['user_profile_full']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserProfile: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user profile */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['user_profile']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserStatistics: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user statistics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_statistics']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserFavorites: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user favorites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: components['schemas']['user_favorites']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserUpdates: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user updates */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_updates']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserAbout: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user about in raw HTML */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_about']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserHistory: { - parameters: { - query?: { - type?: 'anime' | 'manga'; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user history (past 30 days) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_history']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserFriends: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user friends */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_friends']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserAnimelist: { - parameters: { - query?: { - status?: components['schemas']['user_anime_list_status_filter']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user anime list */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserMangaList: { - parameters: { - query?: { - status?: components['schemas']['user_manga_list_status_filter']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user manga list */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserReviews: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user reviews */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data?: { - data?: ( - | ({ - user?: components['schemas']['user_meta']; - } & { - anime?: components['schemas']['anime_meta']; - } & components['schemas']['anime_review']) - | ({ - user?: components['schemas']['user_meta']; - } & { - manga?: components['schemas']['manga_meta']; - } & components['schemas']['manga_review']) - )[]; - } & components['schemas']['pagination']; - }; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserRecommendations: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Recent Anime Recommendations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['recommendations']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserClubs: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user clubs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['user_clubs']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getUserExternal: { - parameters: { - query?: never; - header?: never; - path: { - username: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns user's external links */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['external_links']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getWatchRecentEpisodes: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Recently Added Episodes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['watch_episodes']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getWatchPopularEpisodes: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Popular Episodes */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['watch_episodes']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getWatchRecentPromos: { - parameters: { - query?: { - page?: components['parameters']['page']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Recently Added Promotional Videos */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['watch_promos']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - getWatchPopularPromos: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Returns Popular Promotional Videos */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['watch_promos']; - }; - }; - /** @description Error: Bad request. When required parameters were not supplied. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; -} diff --git a/api/schemas/OpenLibrary.json b/api/schemas/OpenLibrary.json deleted file mode 100644 index 6258f57e..00000000 --- a/api/schemas/OpenLibrary.json +++ /dev/null @@ -1,602 +0,0 @@ -{ - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "title": "Detail", - "type": "array" - } - }, - "title": "HTTPValidationError", - "type": "object" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "type": "string" - }, - "title": "Location", - "type": "array" - }, - "msg": { - "title": "Message", - "type": "string" - }, - "type": { - "title": "Error Type", - "type": "string" - } - }, - "required": ["loc", "msg", "type"], - "title": "ValidationError", - "type": "object" - } - } - }, - "info": { - "description": "- These are still in development and may not be perfect\n- Contribute by proposing edits to [openapi.json](https://github.com/internetarchive/openlibrary/blob/master/static/openapi.json)\n- Please do not use our APIs for bulk downloads, see [dev center](https://openlibrary.org/developers/api)", - "title": "Open Library API", - "version": "0.1.0" - }, - "openapi": "3.0.2", - "paths": { - "/api/books": { - "get": { - "operationId": "read_api_books_api_books_get", - "parameters": [ - { - "examples": { - "isbn": { - "value": "ISBN:0201558025" - }, - "multiple": { - "value": "ISBN:9781408113479,OCLC:420517" - }, - "oclc": { - "value": "OCLC:263296519" - } - }, - "in": "query", - "name": "bibkeys", - "required": true, - "schema": { - "title": "Bibkeys", - "type": "string" - } - }, - { - "description": "Specifies the response format. Possible values are json and javascript. When not specified the format is javascript.", - "in": "query", - "name": "format", - "required": false, - "schema": { - "default": "json", - "title": "Format", - "type": "string" - } - }, - { - "description": "The name of the JavaScript function to call with the result. This is considered only when the format is javascript.", - "in": "query", - "name": "callback", - "required": false, - "schema": { - "title": "Callback" - } - }, - { - "description": "Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi.", - "in": "query", - "name": "jscmd", - "required": false, - "schema": { - "default": "viewapi", - "title": "Jscmd", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Api Books", - "tags": ["books"] - } - }, - "/api/volumes/brief/{key_type}/{value}.json": { - "get": { - "operationId": "read_api_volumes_brief_api_volumes_brief__key_type___value__json_get", - "parameters": [ - { - "in": "path", - "name": "key_type", - "required": true, - "schema": { - "title": "Key Type" - } - }, - { - "in": "path", - "name": "value", - "required": true, - "schema": { - "title": "Value" - } - }, - { - "in": "query", - "name": "callback", - "required": false, - "schema": { - "title": "Callback" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Api Volumes Brief", - "tags": ["books"] - } - }, - "/authors/{olid}.json": { - "get": { - "operationId": "read_authors_authors__olid__json_get", - "parameters": [ - { - "in": "path", - "name": "olid", - "required": true, - "schema": { - "title": "Olid" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Authors", - "tags": ["authors"] - } - }, - "/authors/{olid}/works.json": { - "get": { - "operationId": "read_authors_works_authors__olid__works_json_get", - "parameters": [ - { - "in": "path", - "name": "olid", - "required": true, - "schema": { - "title": "Olid" - } - }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "title": "Limit", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Authors Works", - "tags": ["authors"] - } - }, - "/books/{olid}": { - "get": { - "operationId": "read_books_books__olid__get", - "parameters": [ - { - "in": "path", - "name": "olid", - "required": true, - "schema": { - "example": "OL53924W", - "title": "Olid" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Books", - "tags": ["books"] - } - }, - "/covers/{key_type}/{value}-{size}.jpg": { - "get": { - "operationId": "read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get", - "parameters": [ - { - "in": "path", - "name": "key_type", - "required": true, - "schema": { - "title": "Key Type" - } - }, - { - "in": "path", - "name": "value", - "required": true, - "schema": { - "title": "Value" - } - }, - { - "in": "path", - "name": "size", - "required": true, - "schema": { - "title": "Size" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Covers Key Type Value Size Jpeg", - "tags": ["covers"] - } - }, - "/isbn/{isbn}": { - "get": { - "operationId": "read_isbn_isbn__isbn__get", - "parameters": [ - { - "in": "path", - "name": "isbn", - "required": true, - "schema": { - "title": "Isbn" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Isbn", - "tags": ["books"] - } - }, - "/search.json": { - "get": { - "operationId": "read_search_json_search_json_get", - "parameters": [ - { - "in": "query", - "name": "q", - "required": true, - "schema": { - "title": "Q" - } - }, - { - "in": "query", - "name": "page", - "required": false, - "schema": { - "title": "Page", - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Search Json", - "tags": ["search"] - } - }, - "/search/authors.json": { - "get": { - "operationId": "read_search_authors_json_search_authors_json_get", - "parameters": [ - { - "in": "query", - "name": "q", - "required": true, - "schema": { - "title": "Q" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Search Authors Json", - "tags": ["search"] - } - }, - "/subjects/{subject}.json": { - "get": { - "operationId": "read_subjects_subjects__subject__json_get", - "parameters": [ - { - "in": "path", - "name": "subject", - "required": true, - "schema": { - "title": "Subject" - } - }, - { - "in": "query", - "name": "details", - "required": false, - "schema": { - "default": false, - "title": "Details", - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Subjects", - "tags": ["subjects"] - } - }, - "/works/{olid}": { - "get": { - "operationId": "read_works_works__olid__get", - "parameters": [ - { - "in": "path", - "name": "olid", - "required": true, - "schema": { - "title": "Olid" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "Read Works", - "tags": ["books"] - } - } - }, - "tags": [ - { - "description": "Retrieve a specific work or edition by identifier", - "externalDocs": { - "description": "Find out more", - "url": "https://openlibrary.org/dev/docs/api/books" - }, - "name": "books" - }, - { - "description": "Retrieve an author and their works by author identifier", - "externalDocs": { - "description": "Find out more", - "url": "https://openlibrary.org/dev/docs/api/authors" - }, - "name": "authors" - }, - { - "description": "Search results for books, authors, and more", - "externalDocs": { - "description": "Find out more", - "url": "https://openlibrary.org/dev/docs/api/search" - }, - "name": "search" - }, - { - "description": "Fetch book covers by ISBN or Open Library identifier", - "externalDocs": { - "description": "Find out more", - "url": "https://openlibrary.org/dev/docs/api/covers" - }, - "name": "covers" - }, - { - "description": "Fetch books by subject name ", - "externalDocs": { - "description": "Find out more", - "url": "https://openlibrary.org/dev/docs/api/subjects" - }, - "name": "subjects" - } - ] -} diff --git a/api/schemas/OpenLibrary.ts b/api/schemas/OpenLibrary.ts deleted file mode 100644 index aa6fbbac..00000000 --- a/api/schemas/OpenLibrary.ts +++ /dev/null @@ -1,578 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/api/books': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Api Books */ - get: operations['read_api_books_api_books_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/volumes/brief/{key_type}/{value}.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Api Volumes Brief */ - get: operations['read_api_volumes_brief_api_volumes_brief__key_type___value__json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/authors/{olid}.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Authors */ - get: operations['read_authors_authors__olid__json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/authors/{olid}/works.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Authors Works */ - get: operations['read_authors_works_authors__olid__works_json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/books/{olid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Books */ - get: operations['read_books_books__olid__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/covers/{key_type}/{value}-{size}.jpg': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Covers Key Type Value Size Jpeg */ - get: operations['read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/isbn/{isbn}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Isbn */ - get: operations['read_isbn_isbn__isbn__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/search.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Search Json */ - get: operations['read_search_json_search_json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/search/authors.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Search Authors Json */ - get: operations['read_search_authors_json_search_authors_json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/subjects/{subject}.json': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Subjects */ - get: operations['read_subjects_subjects__subject__json_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/works/{olid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Read Works */ - get: operations['read_works_works__olid__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** ValidationError */ - ValidationError: { - /** Location */ - loc: string[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - read_api_books_api_books_get: { - parameters: { - query: { - bibkeys: string; - /** @description Specifies the response format. Possible values are json and javascript. When not specified the format is javascript. */ - format?: string; - /** @description The name of the JavaScript function to call with the result. This is considered only when the format is javascript. */ - callback?: unknown; - /** @description Decides what information to provide for each matched bib_key. Possible values are viewapi and data. The default value is viewapi. */ - jscmd?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_api_volumes_brief_api_volumes_brief__key_type___value__json_get: { - parameters: { - query?: { - callback?: unknown; - }; - header?: never; - path: { - key_type: unknown; - value: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_authors_authors__olid__json_get: { - parameters: { - query?: never; - header?: never; - path: { - olid: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_authors_works_authors__olid__works_json_get: { - parameters: { - query?: { - limit?: number; - }; - header?: never; - path: { - olid: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_books_books__olid__get: { - parameters: { - query?: never; - header?: never; - path: { - olid: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_covers_key_type_value_size_jpeg_covers__key_type___value___size__jpg_get: { - parameters: { - query?: never; - header?: never; - path: { - key_type: unknown; - value: unknown; - size: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_isbn_isbn__isbn__get: { - parameters: { - query?: never; - header?: never; - path: { - isbn: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_search_json_search_json_get: { - parameters: { - query: { - q: unknown; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_search_authors_json_search_authors_json_get: { - parameters: { - query: { - q: unknown; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_subjects_subjects__subject__json_get: { - parameters: { - query?: { - details?: boolean; - }; - header?: never; - path: { - subject: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - read_works_works__olid__get: { - parameters: { - query?: never; - header?: never; - path: { - olid: unknown; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; -} diff --git a/api/schemas/TMDB.ts b/api/schemas/TMDB.ts deleted file mode 100644 index 6df120e0..00000000 --- a/api/schemas/TMDB.ts +++ /dev/null @@ -1,22832 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/3/authentication': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Validate Key - * @description Test your API Key to see if it's valid. - */ - get: operations['authentication-validate-key']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get the public details of an account on TMDB. - */ - get: operations['account-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/favorite': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add Favorite - * @description Mark a movie or TV show as a favourite. - */ - post: operations['account-add-favorite']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/watchlist': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add To Watchlist - * @description Add a movie or TV show to your watchlist. - */ - post: operations['account-add-to-watchlist']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/favorite/movies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Favorite Movies - * @description Get a users list of favourite movies. - */ - get: operations['account-get-favorites']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/favorite/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Favorite TV - * @description Get a users list of favourite TV shows. - */ - get: operations['account-favorite-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/lists': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Lists - * @description Get a users list of custom lists. - */ - get: operations['account-lists']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/rated/movies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated Movies - * @description Get a users list of rated movies. - */ - get: operations['account-rated-movies']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/rated/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated TV - * @description Get a users list of rated TV shows. - */ - get: operations['account-rated-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/rated/tv/episodes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated TV Episodes - * @description Get a users list of rated TV episodes. - */ - get: operations['account-rated-tv-episodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/watchlist/movies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Watchlist Movies - * @description Get a list of movies added to a users watchlist. - */ - get: operations['account-watchlist-movies']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/account/{account_id}/watchlist/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Watchlist TV - * @description Get a list of TV shows added to a users watchlist. - */ - get: operations['account-watchlist-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/guest_session/new': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Create Guest Session */ - get: operations['authentication-create-guest-session']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/token/new': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Create Request Token */ - get: operations['authentication-create-request-token']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/session/new': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create Session */ - post: operations['authentication-create-session']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/session/convert/4': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create Session (from v4 token) */ - post: operations['authentication-create-session-from-v4-token']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/token/validate_with_login': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create Session (with login) - * @description This method allows an application to validate a request token by entering a username and password. - */ - post: operations['authentication-create-session-from-login']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/authentication/session': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Delete Session */ - delete: operations['authentication-delete-session']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/certification/movie/list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie Certifications - * @description Get an up to date list of the officially supported movie certifications on TMDB. - */ - get: operations['certification-movie-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/certification/tv/list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** TV Certifications */ - get: operations['certifications-tv-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie List - * @description Get a list of all of the movie ids that have been changed in the past 24 hours. - */ - get: operations['changes-movie-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** People List */ - get: operations['changes-people-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** TV List */ - get: operations['changes-tv-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/collection/{collection_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get collection details by ID. - */ - get: operations['collection-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/collection/{collection_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the images that belong to a collection. - */ - get: operations['collection-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/collection/{collection_id}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Translations */ - get: operations['collection-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/company/{company_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get the company details by ID. - */ - get: operations['company-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/company/{company_id}/alternative_names': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Alternative Names - * @description Get the company details by ID. - */ - get: operations['company-alternative-names']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/company/{company_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the company logos by id. - */ - get: operations['company-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Query the API configuration details. - */ - get: operations['configuration-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration/countries': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Countries - * @description Get the list of countries (ISO 3166-1 tags) used throughout TMDB. - */ - get: operations['configuration-countries']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration/jobs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Jobs - * @description Get the list of the jobs and departments we use on TMDB. - */ - get: operations['configuration-jobs']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration/languages': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Languages - * @description Get the list of languages (ISO 639-1 tags) used throughout TMDB. - */ - get: operations['configuration-languages']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration/primary_translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Primary Translations - * @description Get a list of the officially supported translations on TMDB. - */ - get: operations['configuration-primary-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/configuration/timezones': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Timezones - * @description Get the list of timezones used throughout TMDB. - */ - get: operations['configuration-timezones']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/credit/{credit_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get a movie or TV credit details by ID. - */ - get: operations['credit-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/discover/movie': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie - * @description Find movies using over 30 filters and sort options. - */ - get: operations['discover-movie']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/discover/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV - * @description Find TV shows using over 30 filters and sort options. - */ - get: operations['discover-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/find/{external_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Find By ID - * @description Find data by external ID's. - */ - get: operations['find-by-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/genre/movie/list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie List - * @description Get the list of official genres for movies. - */ - get: operations['genre-movie-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/genre/tv/list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV List - * @description Get the list of official genres for TV shows. - */ - get: operations['genre-tv-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/guest_session/{guest_session_id}/rated/movies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated Movies - * @description Get the rated movies for a guest session. - */ - get: operations['guest-session-rated-movies']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/guest_session/{guest_session_id}/rated/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated TV - * @description Get the rated TV shows for a guest session. - */ - get: operations['guest-session-rated-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/guest_session/{guest_session_id}/rated/tv/episodes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Rated TV Episodes - * @description Get the rated TV episodes for a guest session. - */ - get: operations['guest-session-rated-tv-episodes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/keyword/{keyword_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Details */ - get: operations['keyword-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/keyword/{keyword_id}/movies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Movies */ - get: operations['keyword-movies']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list/{list_id}/add_item': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add Movie - * @description Add a movie to a list. - */ - post: operations['list-add-movie']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list/{list_id}/item_status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Check Item Status - * @description Use this method to check if an item has already been added to the list. - */ - get: operations['list-check-item-status']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list/{list_id}/clear': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Clear - * @description Clear all items from a list. - */ - post: operations['list-clear']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create */ - post: operations['list-create']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list/{list_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Details */ - get: operations['list-details']; - put?: never; - post?: never; - /** - * Delete - * @description Delete a list. - */ - delete: operations['list-delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/list/{list_id}/remove_item': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Remove Movie - * @description Remove a movie from a list. - */ - post: operations['list-remove-movie']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/now_playing': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Now Playing - * @description Get a list of movies that are currently in theatres. - */ - get: operations['movie-now-playing-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/popular': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Popular - * @description Get a list of movies ordered by popularity. - */ - get: operations['movie-popular-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/top_rated': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Top Rated - * @description Get a list of movies ordered by rating. - */ - get: operations['movie-top-rated-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/upcoming': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Upcoming - * @description Get a list of movies that are being released soon. - */ - get: operations['movie-upcoming-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get the top level details of a movie by ID. - */ - get: operations['movie-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/account_states': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Account States - * @description Get the rating, watchlist and favourite status of an account. - */ - get: operations['movie-account-states']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/alternative_titles': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Alternative Titles - * @description Get the alternative titles for a movie. - */ - get: operations['movie-alternative-titles']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Changes - * @description Get the recent changes for a movie. - */ - get: operations['movie-changes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Credits */ - get: operations['movie-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/external_ids': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** External IDs */ - get: operations['movie-external-ids']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the images that belong to a movie. - */ - get: operations['movie-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/keywords': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Keywords */ - get: operations['movie-keywords']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/latest': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Latest - * @description Get the newest movie ID. - */ - get: operations['movie-latest-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/lists': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Lists - * @description Get the lists that a movie has been added to. - */ - get: operations['movie-lists']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/recommendations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Recommendations */ - get: operations['movie-recommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/release_dates': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Release Dates - * @description Get the release dates and certifications for a movie. - */ - get: operations['movie-release-dates']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Reviews - * @description Get the user reviews for a movie. - */ - get: operations['movie-reviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/similar': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Similar - * @description Get the similar movies based on genres and keywords. - */ - get: operations['movie-similar']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Translations - * @description Get the translations for a movie. - */ - get: operations['movie-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Videos */ - get: operations['movie-videos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/watch/providers': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Watch Providers - * @description Get the list of streaming providers we have for a movie. - */ - get: operations['movie-watch-providers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/movie/{movie_id}/rating': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add Rating - * @description Rate a movie and save it to your rated list. - */ - post: operations['movie-add-rating']; - /** - * Delete Rating - * @description Delete a user rating. - */ - delete: operations['movie-delete-rating']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/network/{network_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Details */ - get: operations['network-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/network/{network_id}/alternative_names': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Alternative Names - * @description Get the alternative names of a network. - */ - get: operations['details-copy']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/network/{network_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the TV network logos by id. - */ - get: operations['alternative-names-copy']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/popular': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Popular - * @description Get a list of people ordered by popularity. - */ - get: operations['person-popular-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Query the top level details of a person. - */ - get: operations['person-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Changes - * @description Get the recent changes for a person. - */ - get: operations['person-changes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/combined_credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Combined Credits - * @description Get the combined movie and TV credits that belong to a person. - */ - get: operations['person-combined-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/external_ids': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * External IDs - * @description Get the external ID's that belong to a person. - */ - get: operations['person-external-ids']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the profile images that belong to a person. - */ - get: operations['person-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/latest': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Latest - * @description Get the newest created person. This is a live response and will continuously change. - */ - get: operations['person-latest-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/movie_credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie Credits - * @description Get the movie credits for a person. - */ - get: operations['person-movie-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/tv_credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV Credits - * @description Get the TV credits that belong to a person. - */ - get: operations['person-tv-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/tagged_images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Tagged Images - * @description Get the tagged images for a person. - */ - get: operations['person-tagged-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/person/{person_id}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Translations - * @description Get the translations that belong to a person. - */ - get: operations['translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/review/{review_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Retrieve the details of a movie or TV show review. - */ - get: operations['review-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/collection': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Collection - * @description Search for collections by their original, translated and alternative names. - */ - get: operations['search-collection']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/company': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Company - * @description Search for companies by their original and alternative names. - */ - get: operations['search-company']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/keyword': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Keyword - * @description Search for keywords by their name. - */ - get: operations['search-keyword']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/movie': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie - * @description Search for movies by their original, translated and alternative titles. - */ - get: operations['search-movie']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/multi': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Multi - * @description Use multi search when you want to search for movies, TV shows and people in a single request. - */ - get: operations['search-multi']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/person': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Person - * @description Search for people by their name and also known as names. - */ - get: operations['search-person']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/search/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV - * @description Search for TV shows by their original, translated and also known as names. - */ - get: operations['search-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/trending/all/{time_window}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * All - * @description Get the trending movies, TV shows and people. - */ - get: operations['trending-all']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/trending/movie/{time_window}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movies - * @description Get the trending movies on TMDB. - */ - get: operations['trending-movies']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/trending/person/{time_window}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * People - * @description Get the trending people on TMDB. - */ - get: operations['trending-people']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/trending/tv/{time_window}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV - * @description Get the trending TV shows on TMDB. - */ - get: operations['trending-tv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/airing_today': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Airing Today - * @description Get a list of TV shows airing today. - */ - get: operations['tv-series-airing-today-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/on_the_air': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * On The Air - * @description Get a list of TV shows that air in the next 7 days. - */ - get: operations['tv-series-on-the-air-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/popular': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Popular - * @description Get a list of TV shows ordered by popularity. - */ - get: operations['tv-series-popular-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/top_rated': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Top Rated - * @description Get a list of TV shows ordered by rating. - */ - get: operations['tv-series-top-rated-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get the details of a TV show. - */ - get: operations['tv-series-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/account_states': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Account States - * @description Get the rating, watchlist and favourite status. - */ - get: operations['tv-series-account-states']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/aggregate_credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Aggregate Credits - * @description Get the aggregate credits (cast and crew) that have been added to a TV show. - */ - get: operations['tv-series-aggregate-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/alternative_titles': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Alternative Titles - * @description Get the alternative titles that have been added to a TV show. - */ - get: operations['tv-series-alternative-titles']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Changes - * @description Get the recent changes for a TV show. - */ - get: operations['tv-series-changes']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/content_ratings': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Content Ratings - * @description Get the content ratings that have been added to a TV show. - */ - get: operations['tv-series-content-ratings']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Credits - * @description Get the latest season credits of a TV show. - */ - get: operations['tv-series-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/episode_groups': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Episode Groups - * @description Get the episode groups that have been added to a TV show. - */ - get: operations['tv-series-episode-groups']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/external_ids': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * External IDs - * @description Get a list of external IDs that have been added to a TV show. - */ - get: operations['tv-series-external-ids']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the images that belong to a TV series. - */ - get: operations['tv-series-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/keywords': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Keywords - * @description Get a list of keywords that have been added to a TV show. - */ - get: operations['tv-series-keywords']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/latest': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Latest - * @description Get the newest TV show ID. - */ - get: operations['tv-series-latest-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/lists': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Lists - * @description Get the lists that a TV series has been added to. - */ - get: operations['lists-copy']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/recommendations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Recommendations */ - get: operations['tv-series-recommendations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/reviews': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Reviews - * @description Get the reviews that have been added to a TV show. - */ - get: operations['tv-series-reviews']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/screened_theatrically': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Screened Theatrically - * @description Get the seasons and episodes that have screened theatrically. - */ - get: operations['tv-series-screened-theatrically']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/similar': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Similar - * @description Get the similar TV shows. - */ - get: operations['tv-series-similar']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Translations - * @description Get the translations that have been added to a TV show. - */ - get: operations['tv-series-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Videos - * @description Get the videos that belong to a TV show. - */ - get: operations['tv-series-videos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/watch/providers': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Watch Providers - * @description Get the list of streaming providers we have for a TV show. - */ - get: operations['tv-series-watch-providers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/rating': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add Rating - * @description Rate a TV show and save it to your rated list. - */ - post: operations['tv-series-add-rating']; - /** Delete Rating */ - delete: operations['tv-series-delete-rating']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Query the details of a TV season. - */ - get: operations['tv-season-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/account_states': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Account States - * @description Get the rating, watchlist and favourite status. - */ - get: operations['tv-season-account-states']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/aggregate_credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Aggregate Credits - * @description Get the aggregate credits (cast and crew) that have been added to a TV season. - */ - get: operations['tv-season-aggregate-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/season/{season_id}/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Changes - * @description Get the recent changes for a TV season. - */ - get: operations['tv-season-changes-by-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Credits */ - get: operations['tv-season-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/external_ids': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * External IDs - * @description Get a list of external IDs that have been added to a TV season. - */ - get: operations['tv-season-external-ids']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the images that belong to a TV season. - */ - get: operations['tv-season-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Translations - * @description Get the translations for a TV season. - */ - get: operations['tv-season-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Videos - * @description Get the videos that belong to a TV season. - */ - get: operations['tv-season-videos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/watch/providers': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Watch Providers - * @description Get the list of streaming providers we have for a TV season. - */ - get: operations['tv-season-watch-providers']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Query the details of a TV episode. - */ - get: operations['tv-episode-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/account_states': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Account States - * @description Get the rating, watchlist and favourite status. - */ - get: operations['tv-episode-account-states']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/episode/{episode_id}/changes': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Changes - * @description Get the recent changes for a TV episode. - */ - get: operations['tv-episode-changes-by-id']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/credits': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Credits */ - get: operations['tv-episode-credits']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/external_ids': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * External IDs - * @description Get a list of external IDs that have been added to a TV episode. - */ - get: operations['tv-episode-external-ids']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Images - * @description Get the images that belong to a TV episode. - */ - get: operations['tv-episode-images']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Translations - * @description Get the translations that have been added to a TV episode. - */ - get: operations['tv-episode-translations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/videos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Videos - * @description Get the videos that belong to a TV episode. - */ - get: operations['tv-episode-videos']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/{series_id}/season/{season_number}/episode/{episode_number}/rating': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add Rating - * @description Rate a TV episode and save it to your rated list. - */ - post: operations['tv-episode-add-rating']; - /** - * Delete Rating - * @description Delete your rating on a TV episode. - */ - delete: operations['tv-episode-delete-rating']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/tv/episode_group/{tv_episode_group_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Details - * @description Get the details of a TV episode group. - */ - get: operations['tv-episode-group-details']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/watch/providers/regions': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Available Regions - * @description Get the list of the countries we have watch provider (OTT/streaming) data for. - */ - get: operations['watch-providers-available-regions']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/watch/providers/movie': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Movie Providers - * @description Get the list of streaming providers we have for movies. - */ - get: operations['watch-providers-movie-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/3/watch/providers/tv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * TV Providers - * @description Get the list of streaming providers we have for TV shows. - */ - get: operations['watch-provider-tv-list']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: never; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - 'authentication-validate-key': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 7 - */ - status_code: number; - /** @example Invalid API key: You must be granted a valid key. */ - status_message?: string; - /** - * @default true - * @example false - */ - success: boolean; - }; - }; - }; - }; - }; - 'account-details': { - parameters: { - query?: { - session_id?: string; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - avatar?: { - gravatar?: { - /** @example c9e9fc152ee756a900db85757c29815d */ - hash?: string; - }; - tmdb?: { - /** @example /xy44UvpbTgzs9kWmp4C3fEaCl5h.png */ - avatar_path?: string; - }; - }; - /** - * @default 0 - * @example 548 - */ - id: number; - /** @example en */ - iso_639_1?: string; - /** @example CA */ - iso_3166_1?: string; - /** @example Travis Bell */ - name?: string; - /** - * @default true - * @example false - */ - include_adult: boolean; - /** @example travisbell */ - username?: string; - }; - }; - }; - }; - }; - 'account-add-favorite': { - parameters: { - query?: { - session_id?: string; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - }; - }; - 'account-add-to-watchlist': { - parameters: { - query?: { - session_id?: string; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - }; - }; - 'account-get-favorites': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /se5Hxz7PArQZOG3Nx2bpfOhLhtV.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 9806 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example The Incredibles */ - original_title?: string; - /** @example Bob Parr has given up his superhero days to log in time as an insurance adjuster and raise his three children with his formerly heroic wife in suburbia. But when he receives a mysterious assignment, it's time to get back into costume. */ - overview?: string; - /** - * @default 0 - * @example 71.477 - */ - popularity: number; - /** @example /2LqaLgk4Z226KkgPJuiOQ58wvrm.jpg */ - poster_path?: string; - /** @example 2004-10-27 */ - release_date?: string; - /** @example The Incredibles */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.702 - */ - vote_average: number; - /** - * @default 0 - * @example 16162 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 4 - */ - total_pages: number; - /** - * @default 0 - * @example 80 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-favorite-tv': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 1396 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Breaking Bad */ - original_name?: string; - /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ - overview?: string; - /** - * @default 0 - * @example 292.904 - */ - popularity: number; - /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ - poster_path?: string; - /** @example 2008-01-20 */ - first_air_date?: string; - /** @example Breaking Bad */ - name?: string; - /** - * @default 0 - * @example 8.878 - */ - vote_average: number; - /** - * @default 0 - * @example 11548 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 4 - */ - total_pages: number; - /** - * @default 0 - * @example 68 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-lists': { - parameters: { - query?: { - page?: number; - session_id?: string; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example */ - description?: string; - /** - * @default 0 - * @example 0 - */ - favorite_count: number; - /** - * @default 0 - * @example 120174 - */ - id: number; - /** - * @default 0 - * @example 5 - */ - item_count: number; - /** @example en */ - iso_639_1?: string; - /** @example movie */ - list_type?: string; - /** @example Test Alpha Sort */ - name?: string; - poster_path?: unknown; - }[]; - /** - * @default 0 - * @example 2 - */ - total_pages: number; - /** - * @default 0 - * @example 25 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-rated-movies': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /dUVbWINfRMGojGZRcO6GF1Z2nV8.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 120 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example The Lord of the Rings: The Fellowship of the Ring */ - original_title?: string; - /** @example Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed. */ - overview?: string; - /** - * @default 0 - * @example 84.737 - */ - popularity: number; - /** @example /6oom5QYQ2yQTMJIbnvbkBL9cHo6.jpg */ - poster_path?: string; - /** @example 2001-12-18 */ - release_date?: string; - /** @example The Lord of the Rings: The Fellowship of the Ring */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.396 - */ - vote_average: number; - /** - * @default 0 - * @example 22579 - */ - vote_count: number; - /** - * @default 0 - * @example 8 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 47 - */ - total_pages: number; - /** - * @default 0 - * @example 940 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-rated-tv': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /2yZXtM2Kky1Sy0kachbDlwybl3y.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 1705 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Fringe */ - original_name?: string; - /** @example FBI Special Agent Olivia Dunham, brilliant but formerly institutionalized scientist Walter Bishop and his scheming, reluctant son Peter uncover a deadly mystery involving a series of unbelievable events and realize they may be a part of a larger, more disturbing pattern that blurs the line between science fiction and technology. */ - overview?: string; - /** - * @default 0 - * @example 151.906 - */ - popularity: number; - /** @example /sY9hg5dLJ93RJOyKEiu1nAtBRND.jpg */ - poster_path?: string; - /** @example 2008-09-09 */ - first_air_date?: string; - /** @example Fringe */ - name?: string; - /** - * @default 0 - * @example 8.109 - */ - vote_average: number; - /** - * @default 0 - * @example 2050 - */ - vote_count: number; - /** - * @default 0 - * @example 9 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 15 - */ - total_pages: number; - /** - * @default 0 - * @example 290 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-rated-tv-episodes': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example 2013-10-17 */ - air_date?: string; - /** - * @default 0 - * @example 5 - */ - episode_number: number; - /** - * @default 0 - * @example 64782 - */ - id: number; - /** @example The Workplace Proximity */ - name?: string; - /** @example Amy starts working at Caltech which causes friction with Sheldon. Howard agrees with Sheldon who mentions this to Bernadette causing a big fight for the Wolowitzes. */ - overview?: string; - /** @example 4X5305 */ - production_code?: string; - /** - * @default 0 - * @example 22 - */ - runtime: number; - /** - * @default 0 - * @example 7 - */ - season_number: number; - /** - * @default 0 - * @example 1418 - */ - show_id: number; - /** @example /k8atjbd5gAsntuhbPnFpvnvo0qn.jpg */ - still_path?: string; - /** - * @default 0 - * @example 7.242 - */ - vote_average: number; - /** - * @default 0 - * @example 31 - */ - vote_count: number; - /** - * @default 0 - * @example 8 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 10 - */ - total_pages: number; - /** - * @default 0 - * @example 186 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-watchlist-movies': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /rgNzvSagnlc32TuMEBa529QFIig.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 76726 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Chronicle */ - original_title?: string; - /** @example Three high school students make an incredible discovery, leading to their developing uncanny powers beyond their understanding. As they learn to control their abilities and use them to their advantage, their lives start to spin out of control, and their darker sides begin to take over. */ - overview?: string; - /** - * @default 0 - * @example 37.148 - */ - popularity: number; - /** @example /xENglsVIIWEEhhB5lgpy33tGcKI.jpg */ - poster_path?: string; - /** @example 2012-02-01 */ - release_date?: string; - /** @example Chronicle */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 6.822 - */ - vote_average: number; - /** - * @default 0 - * @example 4741 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 34 - */ - total_pages: number; - /** - * @default 0 - * @example 677 - */ - total_results: number; - }; - }; - }; - }; - }; - 'account-watchlist-tv': { - parameters: { - query?: { - language?: string; - page?: number; - session_id?: string; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - account_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /7phlGHRupo38EnuwmkAHdNUqov3.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 58932 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example The Crazy Ones */ - original_name?: string; - /** @example The Crazy Ones is an American situation comedy series created by David E. Kelley that stars Robin Williams and Sarah Michelle Gellar. The single-camera project premiered on CBS on September 26, 2013, as part of the 2013–14 American television season as a Thursday night 9 pm entry. Bill D'Elia, Dean Lorey, Jason Winer, John Montgomery and Mark Teitelbaum serve as executive producers for 20th Century Fox Television. */ - overview?: string; - /** - * @default 0 - * @example 8.939 - */ - popularity: number; - /** @example /s2e7hTrdmNUaJDf0yDP5b4AHvrD.jpg */ - poster_path?: string; - /** @example 2013-09-26 */ - first_air_date?: string; - /** @example The Crazy Ones */ - name?: string; - /** - * @default 0 - * @example 6.176 - */ - vote_average: number; - /** - * @default 0 - * @example 94 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 17 - */ - total_pages: number; - /** - * @default 0 - * @example 325 - */ - total_results: number; - }; - }; - }; - }; - }; - 'authentication-create-guest-session': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** @example 1ce82ec1223641636ad4a60b07de3581 */ - guest_session_id?: string; - /** @example 2016-08-27 16:26:40 UTC */ - expires_at?: string; - }; - }; - }; - }; - }; - 'authentication-create-request-token': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** @example 2016-08-26 17:04:39 UTC */ - expires_at?: string; - /** @example ff5c7eeb5a8870efe3cd7fc5c282cffd26800ecd */ - request_token?: string; - }; - }; - }; - }; - }; - 'authentication-create-session': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** @example 79191836ddaa0da3df76a5ffef6f07ad6ab0c641 */ - session_id?: string; - }; - }; - }; - }; - }; - 'authentication-create-session-from-v4-token': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** @example 2629f70fb498edc263a0adb99118ac41f0053e8c */ - session_id?: string; - }; - }; - }; - }; - }; - 'authentication-create-session-from-login': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - /** @example 2018-07-24 04:10:26 UTC */ - expires_at?: string; - /** @example 1531f1a558c8357ce8990cf887ff196e8f5402ec */ - request_token?: string; - }; - }; - }; - }; - }; - 'authentication-delete-session': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example true - */ - success: boolean; - }; - }; - }; - }; - }; - 'certification-movie-list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - certifications?: { - AU?: { - /** @example E */ - certification?: string; - /** @example Exempt from classification. Films that are exempt from classification must not contain contentious material (i.e. material that would ordinarily be rated M or higher). */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - BG?: { - /** @example D */ - certification?: string; - /** @example Prohibited for persons under 16. */ - meaning?: string; - /** - * @default 0 - * @example 4 - */ - order: number; - }[]; - BR?: { - /** @example 14 */ - certification?: string; - /** @example Not recommended for minors under fourteen. More violent material, stronger sex references and/or nudity. */ - meaning?: string; - /** - * @default 0 - * @example 4 - */ - order: number; - }[]; - CA?: { - /** @example G */ - certification?: string; - /** @example All ages. */ - meaning?: string; - /** - * @default 0 - * @example 2 - */ - order: number; - }[]; - 'CA-QC'?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - DE?: { - /** @example 12 */ - certification?: string; - /** @example Children 12 or older admitted, children between 6 and 11 only when accompanied by parent or a legal guardian. */ - meaning?: string; - /** - * @default 0 - * @example 3 - */ - order: number; - }[]; - DK?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - ES?: { - /** @example A */ - certification?: string; - /** @example General admission. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - FI?: { - /** @example K-16 */ - certification?: string; - /** @example Over 16 years. */ - meaning?: string; - /** - * @default 0 - * @example 4 - */ - order: number; - }[]; - FR?: { - /** @example TP */ - certification?: string; - /** @example Valid for all audiences. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - GB?: { - /** @example 15 */ - certification?: string; - /** @example Only those over 15 years are admitted. Nobody younger than 15 can rent or buy a 15-rated VHS, DVD, Blu-ray Disc, UMD or game, or watch a film in the cinema with this rating. Films under this category can contain adult themes, hard drugs, frequent strong language and limited use of very strong language, strong violence and strong sex references, and nudity without graphic detail. Sexual activity may be portrayed but without any strong detail. Sexual violence may be shown if discreet and justified by context. */ - meaning?: string; - /** - * @default 0 - * @example 5 - */ - order: number; - }[]; - HU?: { - /** @example 6 */ - certification?: string; - /** @example Not recommended below age of 6. */ - meaning?: string; - /** - * @default 0 - * @example 2 - */ - order: number; - }[]; - IN?: { - /** @example U */ - certification?: string; - /** @example Unrestricted Public Exhibition throughout India, suitable for all age groups. Films under this category should not upset children over 4. Such films may contain educational, social or family-oriented themes. Films under this category may also contain fantasy violence and/or mild bad language. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - IT?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - LT?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - MY?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - NL?: { - /** @example AL */ - certification?: string; - /** @example All ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - NO?: { - /** @example 6 */ - certification?: string; - /** @example 6 years (no restriction for children accompanied by an adult). */ - meaning?: string; - /** - * @default 0 - * @example 2 - */ - order: number; - }[]; - NZ?: { - /** @example G */ - certification?: string; - /** @example Suitable for general audiences. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - PH?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - PT?: { - /** @example Públicos */ - certification?: string; - /** @example For all the public (especially designed for children under 3 years of age). */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - RU?: { - /** @example 6+ */ - certification?: string; - /** @example (For children above 6) – Unsuitable for children under 6. */ - meaning?: string; - /** - * @default 0 - * @example 2 - */ - order: number; - }[]; - SE?: { - /** @example 11 */ - certification?: string; - /** @example Children over the age of 7, who are accompanied by an adult, are admitted to films that have been passed for children from the age of 11. */ - meaning?: string; - /** - * @default 0 - * @example 3 - */ - order: number; - }[]; - US?: { - /** @example R */ - certification?: string; - /** @example Under 17 requires accompanying parent or adult guardian 21 or older. The parent/guardian is required to stay with the child under 17 through the entire movie, even if the parent gives the child/teenager permission to see the film alone. These films may contain strong profanity, graphic sexuality, nudity, strong violence, horror, gore, and strong drug use. A movie rated R for profanity often has more severe or frequent language than the PG-13 rating would permit. An R-rated movie may have more blood, gore, drug use, nudity, or graphic sexuality than a PG-13 movie would admit. */ - meaning?: string; - /** - * @default 0 - * @example 4 - */ - order: number; - }[]; - KR?: { - /** @example All */ - certification?: string; - /** @example Film suitable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - SK?: { - /** @example U */ - certification?: string; - /** @example General audience. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - TH?: { - /** @example P */ - certification?: string; - /** @example Educational. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - MX?: { - /** @example AA */ - certification?: string; - /** @example Informative-only rating: Understandable for children under 7 years. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - ID?: { - /** @example SU */ - certification?: string; - /** @example All ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - TR?: { - /** @example Genel İzleyici Kitlesi */ - certification?: string; - /** @example General audience. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - AR?: { - /** @example ATP */ - certification?: string; - /** @example For all public. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - GR?: { - /** @example K */ - certification?: string; - /** @example No restrictions. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - TW?: { - /** @example 0+ */ - certification?: string; - /** @example Viewing is permitted for audiences of all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - ZA?: { - /** @example A */ - certification?: string; - /** @example Suitable for all. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - SG?: { - /** @example G */ - certification?: string; - /** @example Suitable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - IE?: { - /** @example G */ - certification?: string; - /** @example Suitable for children of school going age (note: children can be enrolled in school from the age of 4). */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - PR?: { - /** @example G */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - JP?: { - /** @example G */ - certification?: string; - /** @example General, suitable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - VI?: { - /** @example G */ - certification?: string; - /** @example All ages admitted. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - CH?: { - /** @example 0 */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - IL?: { - /** @example All */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - HK?: { - /** @example I */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - MO?: { - /** @example A */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - LV?: { - /** @example U */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - LU?: { - /** @example EA */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - }; - }; - }; - }; - }; - }; - 'certifications-tv-list': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - certifications?: { - AU?: { - /** @example P */ - certification?: string; - /** @example Programming is intended for younger children 2–11; commercial stations must show at least 30 minutes of P-rated content each weekday and weekends at all times. No advertisements may be shown during P-rated programs. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - BR?: { - /** @example 14 */ - certification?: string; - /** @example Content suitable for viewers over the age of 14. */ - meaning?: string; - /** - * @default 0 - * @example 3 - */ - order: number; - }[]; - CA?: { - /** @example Exempt */ - certification?: string; - /** @example Shows which are exempt from ratings (such as news and sports programming) will not display an on-screen rating at all. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - 'CA-QC'?: { - /** @example 18+ */ - certification?: string; - /** @example Only to be viewed by adults and may contain extreme violence and graphic sexual content. It is mostly used for 18+ movies and pornography. */ - meaning?: string; - /** - * @default 0 - * @example 5 - */ - order: number; - }[]; - DE?: { - /** @example 0 */ - certification?: string; - /** @example Can be aired at any time. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - ES?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - FR?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - GB?: { - /** @example U */ - certification?: string; - /** @example The U symbol stands for Universal. A U film should be suitable for audiences aged four years and over. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - HU?: { - /** @example Unrated */ - certification?: string; - /** @example Without age restriction. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - KR?: { - /** @example Exempt */ - certification?: string; - /** @example This rating is only for knowledge based game shows; lifestyle shows; documentary shows; news; current topic discussion shows; education/culture shows; sports that excludes MMA or other violent sports; and other programs that Korea Communications Standards Commission recognizes. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - LT?: { - /** @example S */ - certification?: string; - /** @example Intended for adult viewers from the age of 18 (corresponding to the age-appropriate index N-18) and broadcast between 23 (11pm) and 6 (6am) hours; Limited to minors and intended for adult audiences. */ - meaning?: string; - /** - * @default 0 - * @example 3 - */ - order: number; - }[]; - NL?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - PH?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - PT?: { - /** @example 12AP */ - certification?: string; - /** @example Acompanhamento Parental (may not be suitable for children under 12, parental guidance advised). */ - meaning?: string; - /** - * @default 0 - * @example 3 - */ - order: number; - }[]; - RU?: { - /** @example 16+ */ - certification?: string; - /** @example Only teens the age of 16 or older can watch. */ - meaning?: string; - /** - * @default 0 - * @example 4 - */ - order: number; - }[]; - SK?: { - /** @example NR */ - certification?: string; - /** @example No rating information. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - TH?: { - /** @example ส */ - certification?: string; - /** @example Sor - Educational movies which the public should be encouraged to see. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - US?: { - /** @example TV-MA */ - certification?: string; - /** @example This program is specifically designed to be viewed by adults and therefore may be unsuitable for children under 17. */ - meaning?: string; - /** - * @default 0 - * @example 6 - */ - order: number; - }[]; - IT?: { - /** @example T */ - certification?: string; - /** @example All ages admitted. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - FI?: { - /** @example S */ - certification?: string; - /** @example Allowed at all times. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - MY?: { - /** @example U */ - certification?: string; - /** @example No age limit. Can be broadcast anytime. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - NZ?: { - /** @example G */ - certification?: string; - /** @example Approved for general viewing. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - NO?: { - /** @example A */ - certification?: string; - /** @example Allowed at all times. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - BG?: { - /** @example Unrated */ - certification?: string; - /** @example Can be viewed for each age. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - MX?: { - /** @example AA */ - certification?: string; - /** @example Aimed at children (can be broadcast anytime). */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - IN?: { - /** @example U */ - certification?: string; - /** @example Viewable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - DK?: { - /** @example A */ - certification?: string; - /** @example Suitable for a general audience. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - SE?: { - /** @example Btl */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - ID?: { - /** @example SU */ - certification?: string; - /** @example Suitable for general audiences over the age of 2 years. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - TR?: { - /** @example Genel İzleyici */ - certification?: string; - /** @example General audience. Suitable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - AR?: { - /** @example ATP */ - certification?: string; - /** @example Suitable for all audiences. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - PL?: { - /** @example 0 */ - certification?: string; - /** @example Positive or neutral view of the world, little to no violence, non-sexual love, and no sexual content. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - MA?: { - /** @example NR */ - certification?: string; - /** @example All audiences. */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - GR?: { - /** @example K */ - certification?: string; - /** @example Suitable for all ages. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - IL?: { - /** @example E */ - certification?: string; - /** @example Exempt from classification. This rating is usually applied to live broadcasts. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - TW?: { - /** @example 0+ */ - certification?: string; - /** @example Suitable for watching by general audiences. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - ZA?: { - /** @example All */ - certification?: string; - /** @example This is a programme/film that does not contain any obscenity, and is suitable for family viewing. A logo must be displayed in the corner of the screen for 30 seconds after each commercial break. */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - SG?: { - /** @example G */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - }[]; - PR?: { - /** @example NR */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - VI?: { - /** @example NR */ - certification?: string; - /** @example */ - meaning?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - }; - }; - }; - }; - }; - }; - 'changes-movie-list': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - /** - * @default 0 - * @example 1120293 - */ - id: number; - /** - * @default true - * @example false - */ - adult: boolean; - }[]; - /** - * @default 0 - * @example 3 - */ - page: number; - /** - * @default 0 - * @example 57 - */ - total_pages: number; - /** - * @default 0 - * @example 5700 - */ - total_results: number; - }; - }; - }; - }; - }; - 'changes-people-list': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - /** - * @default 0 - * @example 4037513 - */ - id: number; - /** - * @default true - * @example false - */ - adult: boolean; - }[]; - /** - * @default 0 - * @example 1 - */ - page: number; - /** - * @default 0 - * @example 53 - */ - total_pages: number; - /** - * @default 0 - * @example 5292 - */ - total_results: number; - }; - }; - }; - }; - }; - 'changes-tv-list': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - /** - * @default 0 - * @example 225591 - */ - id: number; - /** - * @default true - * @example false - */ - adult: boolean; - }[]; - /** - * @default 0 - * @example 1 - */ - page: number; - /** - * @default 0 - * @example 18 - */ - total_pages: number; - /** - * @default 0 - * @example 1763 - */ - total_results: number; - }; - }; - }; - }; - }; - 'collection-details': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - collection_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 10 - */ - id: number; - /** @example Star Wars Collection */ - name?: string; - /** @example en */ - original_language?: string; - /** @example Star Wars Collection */ - original_name?: string; - /** @example An epic space-opera theatrical film series, which depicts the adventures of various characters "a long time ago in a galaxy far, far away…." */ - overview?: string; - /** @example /22dj38IckjzEEUZwN1tPU5VJ1qq.jpg */ - poster_path?: string; - /** @example /4z9ijhgEthfRHShoOvMaBlpciXS.jpg */ - backdrop_path?: string; - parts?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /2w4xG178RpB4MDAIfTkqAuSJzec.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 11 - */ - id: number; - /** @example Star Wars */ - name?: string; - /** @example Star Wars */ - original_name?: string; - /** @example Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire. */ - overview?: string; - /** @example /6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - /** @example en */ - original_language?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 15.8557 - */ - popularity: number; - /** @example 1977-05-25 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.205 - */ - vote_average: number; - /** - * @default 0 - * @example 21522 - */ - vote_count: number; - }[]; - }; - }; - }; - }; - }; - 'collection-images': { - parameters: { - query?: { - /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ - include_image_language?: string; - language?: string; - }; - header?: never; - path: { - collection_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 10 - */ - id: number; - backdrops?: { - /** - * @default 0 - * @example 1.778 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 1080 - */ - height: number; - iso_639_1?: unknown; - /** @example /d8duYyyC9J5T825Hg7grmaabfxQ.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.464 - */ - vote_average: number; - /** - * @default 0 - * @example 30 - */ - vote_count: number; - /** - * @default 0 - * @example 1920 - */ - width: number; - }[]; - posters?: { - /** - * @default 0 - * @example 0.667 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 3000 - */ - height: number; - /** @example en */ - iso_639_1?: string; - /** @example /r8Ph5MYXL04Qzu4QBbq2KjqwtkQ.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.516 - */ - vote_average: number; - /** - * @default 0 - * @example 14 - */ - vote_count: number; - /** - * @default 0 - * @example 2000 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'collection-translations': { - parameters: { - query?: never; - header?: never; - path: { - collection_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 10 - */ - id: number; - translations?: { - /** @example AE */ - iso_3166_1?: string; - /** @example ar */ - iso_639_1?: string; - /** @example العربية */ - name?: string; - /** @example Arabic */ - english_name?: string; - data?: { - /** @example */ - title?: string; - /** @example */ - overview?: string; - /** @example */ - homepage?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'company-details': { - parameters: { - query?: never; - header?: never; - path: { - company_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example */ - description?: string; - /** @example San Francisco, California */ - headquarters?: string; - /** @example https://www.lucasfilm.com */ - homepage?: string; - /** - * @default 0 - * @example 1 - */ - id: number; - /** @example /o86DbpburjxrqAzEDhXZcyE8pDb.png */ - logo_path?: string; - /** @example Lucasfilm Ltd. */ - name?: string; - /** @example US */ - origin_country?: string; - parent_company?: unknown; - }; - }; - }; - }; - }; - 'company-alternative-names': { - parameters: { - query?: never; - header?: never; - path: { - company_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - id: number; - results?: { - /** @example 루카스필름 */ - name?: string; - /** @example */ - type?: string; - }[]; - }; - }; - }; - }; - }; - 'company-images': { - parameters: { - query?: never; - header?: never; - path: { - company_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - id: number; - logos?: { - /** - * @default 0 - * @example 2.97979797979798 - */ - aspect_ratio: number; - /** @example /o86DbpburjxrqAzEDhXZcyE8pDb.png */ - file_path?: string; - /** - * @default 0 - * @example 99 - */ - height: number; - /** @example 5aa080d6c3a3683fea00011e */ - id?: string; - /** @example .svg */ - file_type?: string; - /** - * @default 0 - * @example 5.384 - */ - vote_average: number; - /** - * @default 0 - * @example 2 - */ - vote_count: number; - /** - * @default 0 - * @example 295 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'configuration-details': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - images?: { - /** @example http://image.tmdb.org/t/p/ */ - base_url?: string; - /** @example https://image.tmdb.org/t/p/ */ - secure_base_url?: string; - backdrop_sizes?: string[]; - logo_sizes?: string[]; - poster_sizes?: string[]; - profile_sizes?: string[]; - still_sizes?: string[]; - }; - change_keys?: string[]; - }; - }; - }; - }; - }; - 'configuration-countries': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example AD */ - iso_3166_1?: string; - /** @example Andorra */ - english_name?: string; - /** @example Andorra */ - native_name?: string; - }[]; - }; - }; - }; - }; - 'configuration-jobs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example Production */ - department?: string; - jobs?: string[]; - }[]; - }; - }; - }; - }; - 'configuration-languages': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example bi */ - iso_639_1?: string; - /** @example Bislama */ - english_name?: string; - /** @example */ - name?: string; - }[]; - }; - }; - }; - }; - 'configuration-primary-translations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': string[]; - }; - }; - }; - }; - 'configuration-timezones': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example AD */ - iso_3166_1?: string; - zones?: string[]; - }[]; - }; - }; - }; - }; - 'credit-details': { - parameters: { - query?: never; - header?: never; - path: { - credit_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example cast */ - credit_type?: string; - /** @example Acting */ - department?: string; - /** @example Actor */ - job?: string; - media?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /uDgy6hyPd82kOHh6I95FLtLnj6p.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 100088 - */ - id: number; - /** @example The Last of Us */ - name?: string; - /** @example en */ - original_language?: string; - /** @example The Last of Us */ - original_name?: string; - /** @example Zwanzig Jahre nachdem die moderne Zivilisation zerstört wurde. – Joel, ein abgehärteter Überlebender, wird angeheuert, um Ellie, ein 14-jähriges Mädchen, aus einer bedrückenden Quarantänezone zu schmuggeln. Was als kleiner Job beginnt, wird bald zu einer brutalen, herzzerreißenden Reise, bei der die beiden die USA durchqueren müssen und aufeinander angewiesen sind, um zu überleben. */ - overview?: string; - /** @example /igwIPNClQpGVzb61QlGqcpT5zUy.jpg */ - poster_path?: string; - /** @example tv */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 898.378 - */ - popularity: number; - /** @example 2023-01-15 */ - first_air_date?: string; - /** - * @default 0 - * @example 8.749 - */ - vote_average: number; - /** - * @default 0 - * @example 3341 - */ - vote_count: number; - origin_country?: string[]; - /** @example Joel Miller */ - character?: string; - episodes?: unknown[]; - seasons?: { - /** @example 2023-01-15 */ - air_date?: string; - /** - * @default 0 - * @example 9 - */ - episode_count: number; - /** - * @default 0 - * @example 144593 - */ - id: number; - /** @example Staffel 1 */ - name?: string; - /** @example Die 1. Staffel der Endzeit-Horrorserie The Last of Us feierte ihre Premiere am 15. Januar 2023 bei HBO. In Staffel 1 beginnt für den Überlebenden Joel und das Mädchen Ellie eine Reise durch das postapokalyptische Amerika, in dem Plünderer und mutierte Wesen ihnen nach dem Leben trachten. */ - overview?: string; - /** @example /aUQKIpZZ31KWbpdHMCmaV76u78T.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** - * @default 0 - * @example 100088 - */ - show_id: number; - }[]; - }; - /** @example tv */ - media_type?: string; - /** @example 6024a814c0ae36003d59cc3c */ - id?: string; - person?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 1253360 - */ - id: number; - /** @example Pedro Pascal */ - name?: string; - /** @example Pedro Pascal */ - original_name?: string; - /** @example person */ - media_type?: string; - /** - * @default 0 - * @example 106.095 - */ - popularity: number; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** @example Acting */ - known_for_department?: string; - /** @example /dBOrm29cr7NUrjiDQMTtrTyDpfy.jpg */ - profile_path?: string; - }; - }; - }; - }; - }; - }; - 'discover-movie': { - parameters: { - query?: { - /** @description use in conjunction with `region` */ - certification?: string; - /** @description use in conjunction with `region` */ - 'certification.gte'?: string; - /** @description use in conjunction with `region` */ - 'certification.lte'?: string; - /** @description use in conjunction with the `certification`, `certification.gte` and `certification.lte` filters */ - certification_country?: string; - include_adult?: boolean; - include_video?: boolean; - language?: string; - page?: number; - primary_release_year?: number; - 'primary_release_date.gte'?: string; - 'primary_release_date.lte'?: string; - region?: string; - 'release_date.gte'?: string; - 'release_date.lte'?: string; - sort_by?: - | 'original_title.asc' - | 'original_title.desc' - | 'popularity.asc' - | 'popularity.desc' - | 'revenue.asc' - | 'revenue.desc' - | 'primary_release_date.asc' - | 'title.asc' - | 'title.desc' - | 'primary_release_date.desc' - | 'vote_average.asc' - | 'vote_average.desc' - | 'vote_count.asc' - | 'vote_count.desc'; - 'vote_average.gte'?: number; - 'vote_average.lte'?: number; - 'vote_count.gte'?: number; - 'vote_count.lte'?: number; - /** @description use in conjunction with `with_watch_monetization_types ` or `with_watch_providers ` */ - watch_region?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_cast?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_companies?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_crew?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_genres?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_keywords?: string; - with_origin_country?: string; - with_original_language?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_people?: string; - /** @description possible values are: [1, 2, 3, 4, 5, 6] can be a comma (`AND`) or pipe (`OR`) separated query, can be used in conjunction with `region` */ - with_release_type?: number; - 'with_runtime.gte'?: number; - 'with_runtime.lte'?: number; - /** @description possible values are: [flatrate, free, ads, rent, buy] use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ - with_watch_monetization_types?: string; - /** @description use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ - with_watch_providers?: string; - without_companies?: string; - without_genres?: string; - without_keywords?: string; - without_watch_providers?: string; - year?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /8YFL5QQVPy3AgrEQxNYVSgiPEbe.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 640146 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - original_title?: string; - /** @example Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible. */ - overview?: string; - /** - * @default 0 - * @example 9272.643 - */ - popularity: number; - /** @example /ngl2FKBlU4fhbdsrtdom9LVLBXw.jpg */ - poster_path?: string; - /** @example 2023-02-15 */ - release_date?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 6.5 - */ - vote_average: number; - /** - * @default 0 - * @example 1856 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 38020 - */ - total_pages: number; - /** - * @default 0 - * @example 760385 - */ - total_results: number; - }; - }; - }; - }; - }; - 'discover-tv': { - parameters: { - query?: { - 'air_date.gte'?: string; - 'air_date.lte'?: string; - first_air_date_year?: number; - 'first_air_date.gte'?: string; - 'first_air_date.lte'?: string; - include_adult?: boolean; - include_null_first_air_dates?: boolean; - language?: string; - page?: number; - screened_theatrically?: boolean; - sort_by?: - | 'first_air_date.asc' - | 'first_air_date.desc' - | 'name.asc' - | 'name.desc' - | 'original_name.asc' - | 'original_name.desc' - | 'popularity.asc' - | 'popularity.desc' - | 'vote_average.asc' - | 'vote_average.desc' - | 'vote_count.asc' - | 'vote_count.desc'; - timezone?: string; - 'vote_average.gte'?: number; - 'vote_average.lte'?: number; - 'vote_count.gte'?: number; - 'vote_count.lte'?: number; - /** @description use in conjunction with `with_watch_monetization_types ` or `with_watch_providers ` */ - watch_region?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_companies?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_genres?: string; - /** @description can be a comma (`AND`) or pipe (`OR`) separated query */ - with_keywords?: string; - with_networks?: number; - with_origin_country?: string; - with_original_language?: string; - 'with_runtime.gte'?: number; - 'with_runtime.lte'?: number; - /** @description possible values are: [0, 1, 2, 3, 4, 5], can be a comma (`AND`) or pipe (`OR`) separated query */ - with_status?: string; - /** @description possible values are: [flatrate, free, ads, rent, buy] use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ - with_watch_monetization_types?: string; - /** @description use in conjunction with `watch_region`, can be a comma (`AND`) or pipe (`OR`) separated query */ - with_watch_providers?: string; - without_companies?: string; - without_genres?: string; - without_keywords?: string; - without_watch_providers?: string; - /** @description possible values are: [0, 1, 2, 3, 4, 5, 6], can be a comma (`AND`) or pipe (`OR`) separated query */ - with_type?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ - backdrop_path?: string; - /** @example 2023-01-23 */ - first_air_date?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 202250 - */ - id: number; - /** @example Dirty Linen */ - name?: string; - origin_country?: string[]; - /** @example tl */ - original_language?: string; - /** @example Dirty Linen */ - original_name?: string; - /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ - overview?: string; - /** - * @default 0 - * @example 2684.061 - */ - popularity: number; - /** @example /ujlkQtHAVShWyWTloGU2Vh5Jbo9.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 5 - */ - vote_average: number; - /** - * @default 0 - * @example 13 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 7414 - */ - total_pages: number; - /** - * @default 0 - * @example 148265 - */ - total_results: number; - }; - }; - }; - }; - }; - 'find-by-id': { - parameters: { - query: { - external_source: '' | 'imdb_id' | 'facebook_id' | 'instagram_id' | 'tvdb_id' | 'tiktok_id' | 'twitter_id' | 'wikidata_id' | 'youtube_id'; - language?: string; - }; - header?: never; - path: { - external_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - movie_results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 934433 - */ - id: number; - /** @example Scream VI */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Scream VI */ - original_title?: string; - /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ - overview?: string; - /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 853.917 - */ - popularity: number; - /** @example 2023-03-08 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.388 - */ - vote_average: number; - /** - * @default 0 - * @example 708 - */ - vote_count: number; - }[]; - person_results?: unknown[]; - tv_results?: unknown[]; - tv_episode_results?: unknown[]; - tv_season_results?: unknown[]; - }; - }; - }; - }; - }; - 'genre-movie-list': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - genres?: { - /** - * @default 0 - * @example 28 - */ - id: number; - /** @example Action */ - name?: string; - }[]; - }; - }; - }; - }; - }; - 'genre-tv-list': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - genres?: { - /** - * @default 0 - * @example 10759 - */ - id: number; - /** @example Action & Adventure */ - name?: string; - }[]; - }; - }; - }; - }; - }; - 'guest-session-rated-movies': { - parameters: { - query?: { - language?: string; - page?: number; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - guest_session_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /ikR2qy9xJCHX7M8i5rcvuNfdYXs.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 16 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Dancer in the Dark */ - original_title?: string; - /** @example Selma, a Czech immigrant on the verge of blindness, struggles to make ends meet for herself and her son, who has inherited the same genetic disorder and will suffer the same fate without an expensive operation. When life gets too difficult, Selma learns to cope through her love of musicals, escaping life's troubles - even if just for a moment - by dreaming up little numbers to the rhythmic beats of her surroundings. */ - overview?: string; - /** - * @default 0 - * @example 14.684 - */ - popularity: number; - /** @example /8Wdd3fQfbbQeoSfWpHrDfaFNhBU.jpg */ - poster_path?: string; - /** @example 2000-06-30 */ - release_date?: string; - /** @example Dancer in the Dark */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.885 - */ - vote_average: number; - /** - * @default 0 - * @example 1549 - */ - vote_count: number; - /** - * @default 0 - * @example 8.5 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'guest-session-rated-tv': { - parameters: { - query?: { - language?: string; - page?: number; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - guest_session_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /2OMB0ynKlyIenMJWI2Dy9IWT4c.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 1399 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Game of Thrones */ - original_name?: string; - /** @example Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond. */ - overview?: string; - /** - * @default 0 - * @example 404.299 - */ - popularity: number; - /** @example /7WUHnWGx5OO145IRxPDUkQSh4C7.jpg */ - poster_path?: string; - /** @example 2011-04-17 */ - first_air_date?: string; - /** @example Game of Thrones */ - name?: string; - /** - * @default 0 - * @example 8.436 - */ - vote_average: number; - /** - * @default 0 - * @example 21025 - */ - vote_count: number; - /** - * @default 0 - * @example 8.5 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'guest-session-rated-tv-episodes': { - parameters: { - query?: { - language?: string; - page?: number; - sort_by?: 'created_at.asc' | 'created_at.desc'; - }; - header?: never; - path: { - guest_session_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example 2011-04-17 */ - air_date?: string; - /** - * @default 0 - * @example 1 - */ - episode_number: number; - /** - * @default 0 - * @example 63056 - */ - id: number; - /** @example Winter Is Coming */ - name?: string; - /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ - overview?: string; - /** @example 101 */ - production_code?: string; - /** - * @default 0 - * @example 62 - */ - runtime: number; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** - * @default 0 - * @example 1399 - */ - show_id: number; - /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ - still_path?: string; - /** - * @default 0 - * @example 7.843 - */ - vote_average: number; - /** - * @default 0 - * @example 286 - */ - vote_count: number; - /** - * @default 0 - * @example 8.5 - */ - rating: number; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'keyword-details': { - parameters: { - query?: never; - header?: never; - path: { - keyword_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1701 - */ - id: number; - /** @example hero */ - name?: string; - }; - }; - }; - }; - }; - 'keyword-movies': { - parameters: { - query?: { - include_adult?: boolean; - language?: string; - page?: number; - }; - header?: never; - path: { - keyword_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1701 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /3CxUndGhUcZdt1Zggjdb2HkLLQX.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 640146 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - original_title?: string; - /** @example Das Superhelden-Duo Scott Lang und Hope Van Dyne erkundet zusammen mit Hopes Eltern Hank Pym und Janet Van Dyne das Quantenreich, interagiert mit seltsamen neuen Kreaturen und begibt sich auf ein Abenteuer, das sie über die Grenzen dessen hinaustreiben wird, was sie für möglich gehalten haben. */ - overview?: string; - /** - * @default 0 - * @example 9200.005 - */ - popularity: number; - /** @example /nA5otwVxAfpBP4PVgeuBk3qHcLY.jpg */ - poster_path?: string; - /** @example 2023-02-15 */ - release_date?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 6.5 - */ - vote_average: number; - /** - * @default 0 - * @example 2079 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 11 - */ - total_pages: number; - /** - * @default 0 - * @example 211 - */ - total_results: number; - }; - }; - }; - }; - }; - 'list-add-movie': { - parameters: { - query: { - session_id: string; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY?: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 12 - */ - status_code: number; - /** @example The item/record was updated successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'list-check-item-status': { - parameters: { - query?: { - language?: string; - movie_id?: number; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - id: number; - /** - * @default true - * @example true - */ - item_present: boolean; - }; - }; - }; - }; - }; - 'list-clear': { - parameters: { - query: { - session_id: string; - confirm: boolean; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 12 - */ - status_code: number; - /** @example The item/record was updated successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'list-create': { - parameters: { - query: { - session_id: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example The item/record was created successfully. */ - status_message?: string; - /** - * @default true - * @example true - */ - success: boolean; - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** - * @default 0 - * @example 5861 - */ - list_id: number; - }; - }; - }; - }; - }; - 'list-details': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example travisbell */ - created_by?: string; - /** @example The idea behind this list is to collect the live action comic book movies from within the Marvel franchise. */ - description?: string; - /** - * @default 0 - * @example 0 - */ - favorite_count: number; - /** @example 1 */ - id?: string; - items?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /14QbnygCuTO0vl7CAFmPf1fgZfV.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 634649 - */ - id: number; - /** @example movie */ - media_type?: string; - /** @example en */ - original_language?: string; - /** @example Spider-Man: No Way Home */ - original_title?: string; - /** @example Peter Parker ist demaskiert und kann sein normales Leben nicht mehr von den hohen Einsätzen als Superheld trennen. Als er Doctor Strange um Hilfe bittet, wird die Lage noch gefährlicher und er muss entdecken, was es wirklich bedeutet, Spider-Man zu sein. */ - overview?: string; - /** - * @default 0 - * @example 398.217 - */ - popularity: number; - /** @example /iNKf4D0AzOj9GLq8ZyG3WZaqibL.jpg */ - poster_path?: string; - /** @example 2021-12-15 */ - release_date?: string; - /** @example Spider-Man: No Way Home */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8 - */ - vote_average: number; - /** - * @default 0 - * @example 17267 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 59 - */ - item_count: number; - /** @example en */ - iso_639_1?: string; - /** @example The Marvel Universe */ - name?: string; - /** @example /coJVIUEOToAEGViuhclM7pXC75R.jpg */ - poster_path?: string; - }; - }; - }; - }; - }; - 'list-delete': { - parameters: { - query: { - session_id: string; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 12 - */ - status_code: number; - /** @example The item/record was updated successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'list-remove-movie': { - parameters: { - query: { - session_id: string; - }; - header?: never; - path: { - list_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 13 - */ - status_code: number; - /** @example The item/record was deleted successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'movie-now-playing-list': { - parameters: { - query?: { - language?: string; - page?: number; - /** @description ISO-3166-1 code */ - region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - dates?: { - /** @example 2023-05-03 */ - maximum?: string; - /** @example 2023-03-16 */ - minimum?: string; - }; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /iJQIbOPm81fPEGKt5BPuZmfnA54.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 502356 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example The Super Mario Bros. Movie */ - original_title?: string; - /** @example While working underground to fix a water main, Brooklyn plumbers—and brothers—Mario and Luigi are transported down a mysterious pipe and wander into a magical new world. But when the brothers are separated, Mario embarks on an epic quest to find Luigi. */ - overview?: string; - /** - * @default 0 - * @example 6572.614 - */ - popularity: number; - /** @example /qNBAXBIQlnOThrVvA6mA2B5ggV6.jpg */ - poster_path?: string; - /** @example 2023-04-05 */ - release_date?: string; - /** @example The Super Mario Bros. Movie */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.5 - */ - vote_average: number; - /** - * @default 0 - * @example 1456 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 87 - */ - total_pages: number; - /** - * @default 0 - * @example 1734 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-popular-list': { - parameters: { - query?: { - language?: string; - page?: number; - /** @description ISO-3166-1 code */ - region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /gMJngTNfaqCSCqGD4y8lVMZXKDn.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 640146 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - original_title?: string; - /** @example Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible. */ - overview?: string; - /** - * @default 0 - * @example 8567.865 - */ - popularity: number; - /** @example /ngl2FKBlU4fhbdsrtdom9LVLBXw.jpg */ - poster_path?: string; - /** @example 2023-02-15 */ - release_date?: string; - /** @example Ant-Man and the Wasp: Quantumania */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 6.5 - */ - vote_average: number; - /** - * @default 0 - * @example 1886 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 38029 - */ - total_pages: number; - /** - * @default 0 - * @example 760569 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-top-rated-list': { - parameters: { - query?: { - language?: string; - page?: number; - /** @description ISO-3166-1 code */ - region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /tmU7GeKVybMWFButWEGl2M4GeiP.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 238 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example The Godfather */ - original_title?: string; - /** @example Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge. */ - overview?: string; - /** - * @default 0 - * @example 100.932 - */ - popularity: number; - /** @example /3bhkrj58Vtu7enYsRolD1fZdja1.jpg */ - poster_path?: string; - /** @example 1972-03-14 */ - release_date?: string; - /** @example The Godfather */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.7 - */ - vote_average: number; - /** - * @default 0 - * @example 17806 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 552 - */ - total_pages: number; - /** - * @default 0 - * @example 11032 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-upcoming-list': { - parameters: { - query?: { - language?: string; - page?: number; - /** @description ISO-3166-1 code */ - region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - dates?: { - /** @example 2023-05-23 */ - maximum?: string; - /** @example 2023-05-04 */ - minimum?: string; - }; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /7bWxAsNPv9CXHOhZbJVlj2KxgfP.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 713704 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Evil Dead Rise */ - original_title?: string; - /** @example Two sisters find an ancient vinyl that gives birth to bloodthirsty demons that run amok in a Los Angeles apartment building and thrusts them into a primal battle for survival as they face the most nightmarish version of family imaginable. */ - overview?: string; - /** - * @default 0 - * @example 1696.367 - */ - popularity: number; - /** @example /mIBCtPvKZQlxubxKMeViO2UrP3q.jpg */ - poster_path?: string; - /** @example 2023-04-12 */ - release_date?: string; - /** @example Evil Dead Rise */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7 - */ - vote_average: number; - /** - * @default 0 - * @example 207 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 19 - */ - total_pages: number; - /** - * @default 0 - * @example 369 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-details': { - parameters: { - query?: { - /** @description comma separated list of endpoints within this namespace, 20 items max */ - append_to_response?: string; - language?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ - backdrop_path?: string; - belongs_to_collection?: unknown; - /** - * @default 0 - * @example 63000000 - */ - budget: number; - genres?: { - /** - * @default 0 - * @example 18 - */ - id: number; - /** @example Drama */ - name?: string; - }[]; - /** @example http://www.foxmovies.com/movies/fight-club */ - homepage?: string; - /** - * @default 0 - * @example 550 - */ - id: number; - /** @example tt0137523 */ - imdb_id?: string; - /** @example en */ - original_language?: string; - /** @example Fight Club */ - original_title?: string; - /** @example A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion. */ - overview?: string; - /** - * @default 0 - * @example 61.416 - */ - popularity: number; - /** @example /pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg */ - poster_path?: string; - production_companies?: { - /** - * @default 0 - * @example 508 - */ - id: number; - /** @example /7cxRWzi4LsVm4Utfpr1hfARNurT.png */ - logo_path?: string; - /** @example Regency Enterprises */ - name?: string; - /** @example US */ - origin_country?: string; - }[]; - production_countries?: { - /** @example US */ - iso_3166_1?: string; - /** @example United States of America */ - name?: string; - }[]; - /** @example 1999-10-15 */ - release_date?: string; - /** - * @default 0 - * @example 100853753 - */ - revenue: number; - /** - * @default 0 - * @example 139 - */ - runtime: number; - spoken_languages?: { - /** @example English */ - english_name?: string; - /** @example en */ - iso_639_1?: string; - /** @example English */ - name?: string; - }[]; - /** @example Released */ - status?: string; - /** @example Mischief. Mayhem. Soap. */ - tagline?: string; - /** @example Fight Club */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.433 - */ - vote_average: number; - /** - * @default 0 - * @example 26280 - */ - vote_count: number; - }; - }; - }; - }; - }; - 'movie-account-states': { - parameters: { - query?: { - session_id?: string; - guest_session_id?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** - * @default true - * @example true - */ - favorite: boolean; - rated?: { - /** - * @default 0 - * @example 9 - */ - value: number; - }; - /** - * @default true - * @example false - */ - watchlist: boolean; - }; - }; - }; - }; - }; - 'movie-alternative-titles': { - parameters: { - query?: { - /** @description specify a ISO-3166-1 value to filter the results */ - country?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - titles?: { - /** @example RS */ - iso_3166_1?: string; - /** @example Borilački klub */ - title?: string; - /** @example */ - type?: string; - }[]; - }; - }; - }; - }; - }; - 'movie-changes': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - changes?: { - /** @example images */ - key?: string; - items?: { - /** @example 643197b96dea3a00d4377270 */ - id?: string; - /** @example added */ - action?: string; - /** @example 2023-04-08 16:35:05 UTC */ - time?: string; - /** @example */ - iso_639_1?: string; - /** @example */ - iso_3166_1?: string; - value?: { - poster?: { - /** @example /s9ZrHprviFCx3azfWNBtt1LPSnL.jpg */ - file_path?: string; - }; - }; - }[]; - }[]; - }; - }; - }; - }; - }; - 'movie-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 819 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Edward Norton */ - name?: string; - /** @example Edward Norton */ - original_name?: string; - /** - * @default 0 - * @example 26.99 - */ - popularity: number; - /** @example /8nytsqL59SFJTVYVrN72k6qkGgJ.jpg */ - profile_path?: string; - /** - * @default 0 - * @example 4 - */ - cast_id: number; - /** @example The Narrator */ - character?: string; - /** @example 52fe4250c3a36847f80149f3 */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 376 - */ - id: number; - /** @example Production */ - known_for_department?: string; - /** @example Arnon Milchan */ - name?: string; - /** @example Arnon Milchan */ - original_name?: string; - /** - * @default 0 - * @example 2.931 - */ - popularity: number; - /** @example /b2hBExX4NnczNAnLuTBF4kmNhZm.jpg */ - profile_path?: string; - /** @example 55731b8192514111610027d7 */ - credit_id?: string; - /** @example Production */ - department?: string; - /** @example Executive Producer */ - job?: string; - }[]; - }; - }; - }; - }; - }; - 'movie-external-ids': { - parameters: { - query?: never; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** @example tt0137523 */ - imdb_id?: string; - wikidata_id?: unknown; - /** @example FightClub */ - facebook_id?: string; - instagram_id?: unknown; - twitter_id?: unknown; - }; - }; - }; - }; - }; - 'movie-images': { - parameters: { - query?: { - /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ - include_image_language?: string; - language?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - backdrops?: { - /** - * @default 0 - * @example 1.778 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 800 - */ - height: number; - iso_639_1?: unknown; - /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.622 - */ - vote_average: number; - /** - * @default 0 - * @example 20 - */ - vote_count: number; - /** - * @default 0 - * @example 1422 - */ - width: number; - }[]; - /** - * @default 0 - * @example 550 - */ - id: number; - logos?: { - /** - * @default 0 - * @example 5.203 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 79 - */ - height: number; - /** @example he */ - iso_639_1?: string; - /** @example /c1KLulrIhUqY5fT42nmC5aERGCp.png */ - file_path?: string; - /** - * @default 0 - * @example 5.312 - */ - vote_average: number; - /** - * @default 0 - * @example 1 - */ - vote_count: number; - /** - * @default 0 - * @example 411 - */ - width: number; - }[]; - posters?: { - /** - * @default 0 - * @example 0.667 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 900 - */ - height: number; - /** @example pt */ - iso_639_1?: string; - /** @example /r3pPehX4ik8NLYPpbDRAh0YRtMb.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.258 - */ - vote_average: number; - /** - * @default 0 - * @example 6 - */ - vote_count: number; - /** - * @default 0 - * @example 600 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'movie-keywords': { - parameters: { - query?: never; - header?: never; - path: { - movie_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - keywords?: { - /** - * @default 0 - * @example 818 - */ - id: number; - /** @example based on novel or book */ - name?: string; - }[]; - }; - }; - }; - }; - }; - 'movie-latest-id': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - backdrop_path?: unknown; - belongs_to_collection?: unknown; - /** - * @default 0 - * @example 0 - */ - budget: number; - genres?: unknown[]; - /** @example */ - homepage?: string; - /** - * @default 0 - * @example 1119232 - */ - id: number; - imdb_id?: unknown; - /** @example fr */ - original_language?: string; - /** @example König Charles III */ - original_title?: string; - /** @example */ - overview?: string; - /** - * @default 0 - * @example 0 - */ - popularity: number; - poster_path?: unknown; - production_companies?: unknown[]; - production_countries?: unknown[]; - /** @example */ - release_date?: string; - /** - * @default 0 - * @example 0 - */ - revenue: number; - /** - * @default 0 - * @example 0 - */ - runtime: number; - spoken_languages?: unknown[]; - /** @example Released */ - status?: string; - /** @example */ - tagline?: string; - /** @example König Charles III */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 0 - */ - vote_average: number; - /** - * @default 0 - * @example 0 - */ - vote_count: number; - }; - }; - }; - }; - }; - 'movie-lists': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example Movies I own */ - description?: string; - /** - * @default 0 - * @example 0 - */ - favorite_count: number; - /** - * @default 0 - * @example 8248696 - */ - id: number; - /** - * @default 0 - * @example 409 - */ - item_count: number; - /** @example en */ - iso_639_1?: string; - /** @example movie */ - list_type?: string; - /** @example My Movies */ - name?: string; - poster_path?: unknown; - }[]; - /** - * @default 0 - * @example 122 - */ - total_pages: number; - /** - * @default 0 - * @example 2422 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-recommendations': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': Record; - }; - }; - }; - }; - 'movie-release-dates': { - parameters: { - query?: never; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - results?: { - /** @example BG */ - iso_3166_1?: string; - release_dates?: { - /** @example c */ - certification?: string; - descriptors?: unknown[]; - /** @example */ - iso_639_1?: string; - /** @example */ - note?: string; - /** @example 2012-08-28T00:00:00.000Z */ - release_date?: string; - /** - * @default 0 - * @example 3 - */ - type: number; - }[]; - }[]; - }; - }; - }; - }; - }; - 'movie-reviews': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example Goddard */ - author?: string; - author_details?: { - /** @example */ - name?: string; - /** @example Goddard */ - username?: string; - /** @example /https://secure.gravatar.com/avatar/f248ec34f953bc62cafcbdd81fddd6b6.jpg */ - avatar_path?: string; - rating?: unknown; - }; - /** @example Pretty awesome movie. It shows what one crazy person can convince other crazy people to do. Everyone needs something to believe in. I recommend Jesus Christ, but they want Tyler Durden. */ - content?: string; - /** @example 2018-06-09T17:51:53.359Z */ - created_at?: string; - /** @example 5b1c13b9c3a36848f2026384 */ - id?: string; - /** @example 2021-06-23T15:58:09.421Z */ - updated_at?: string; - /** @example https://www.themoviedb.org/review/5b1c13b9c3a36848f2026384 */ - url?: string; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 8 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-similar': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /3YAldML4EDyoC6RBpzceALigrAZ.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 9300 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Orlando */ - original_title?: string; - /** @example England, 1600. Queen Elizabeth I promises Orlando, a young nobleman obsessed with poetry, that she will grant him land and fortune if he agrees to satisfy a very particular request. */ - overview?: string; - /** - * @default 0 - * @example 7.768 - */ - popularity: number; - /** @example /xvz0qZkXXMq3dH2Revxii8drxWc.jpg */ - poster_path?: string; - /** @example 1992-12-11 */ - release_date?: string; - /** @example Orlando */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 6.966 - */ - vote_average: number; - /** - * @default 0 - * @example 262 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 364 - */ - total_pages: number; - /** - * @default 0 - * @example 7269 - */ - total_results: number; - }; - }; - }; - }; - }; - 'movie-translations': { - parameters: { - query?: never; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - translations?: { - /** @example SA */ - iso_3166_1?: string; - /** @example ar */ - iso_639_1?: string; - /** @example العربية */ - name?: string; - /** @example Arabic */ - english_name?: string; - data?: { - /** @example */ - homepage?: string; - /** @example إدوارد يتعرض لضغوط حتى يصل به الحال إلى أنه لا يستطيع النوم لفتراتٍ طويلة، لكنه يجد بعض السلام في جلسات العلاج النفسي الجماعي، يتعرف إدوارد على أحد الأشخاص وهو (تايلر ديردن) الذي يحرره من تعلقه بالأشياء الذي تستعبده ،ثم يحرره من خوفه من الناس. يقومان معًا بإنشاء نادي القتال الذي يجذب الكثير من الأفراد المحبطين ،الذين يقومون بإخراج طاقة غضبهم وكرههم للعالم في القتال. */ - overview?: string; - /** - * @default 0 - * @example 0 - */ - runtime: number; - /** @example */ - tagline?: string; - /** @example */ - title?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'movie-videos': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - results?: { - /** @example en */ - iso_639_1?: string; - /** @example US */ - iso_3166_1?: string; - /** @example Fight Club (1999) Trailer - Starring Brad Pitt, Edward Norton, Helena Bonham Carter */ - name?: string; - /** @example O-b2VfmmbyA */ - key?: string; - /** @example YouTube */ - site?: string; - /** - * @default 0 - * @example 720 - */ - size: number; - /** @example Trailer */ - type?: string; - /** - * @default true - * @example false - */ - official: boolean; - /** @example 2016-03-05T02:03:14.000Z */ - published_at?: string; - /** @example 639d5326be6d88007f170f44 */ - id?: string; - }[]; - }; - }; - }; - }; - }; - 'movie-watch-providers': { - parameters: { - query?: never; - header?: never; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - results?: { - AE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AE */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - AL?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AL */ - link?: string; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - AR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AR */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - AT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AT */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - rent?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - AU?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=AU */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - BA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BA */ - link?: string; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - BB?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BB */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - BE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BE */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - BG?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BG */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - BH?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BH */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - BO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BO */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - BR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BR */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - BS?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=BS */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - CA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CA */ - link?: string; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /sB5vHrmYmliwUvBwZe8HpXo9r8m.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 305 - */ - provider_id: number; - /** @example Crave Starz */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - CH?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CH */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /rVOOhp6V8FheEAKtFAJMLMbnaMZ.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 150 - */ - provider_id: number; - /** @example blue TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - rent?: { - /** @example /rVOOhp6V8FheEAKtFAJMLMbnaMZ.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 150 - */ - provider_id: number; - /** @example blue TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - CL?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CL */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - CO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CO */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - CR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CR */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - CV?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CV */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - }; - CZ?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=CZ */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /wTF37o4jOkQfjnWe41gmeuASYZA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 308 - */ - provider_id: number; - /** @example O2 TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - DE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DE */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - DK?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DK */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - DO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=DO */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - EC?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EC */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - EE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EE */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - EG?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=EG */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - ES?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ES */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - ads?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - FI?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FI */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - FJ?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FJ */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - FR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=FR */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - GB?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GB */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - rent?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - GF?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GF */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - GI?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GI */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - GR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GR */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - GT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=GT */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - HK?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HK */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - HN?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HN */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - HR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HR */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - ads?: { - /** @example /xrHrIraInfRXnrz1zHhY1tXJowg.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 572 - */ - provider_id: number; - /** @example RTL Play */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - HU?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=HU */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - ID?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ID */ - link?: string; - flatrate?: { - /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 122 - */ - provider_id: number; - /** @example Hotstar */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - IE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IE */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - IL?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IL */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - IN?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IN */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - IQ?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IQ */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - IS?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IS */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - IT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=IT */ - link?: string; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - rent?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - JM?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JM */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - JO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JO */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - JP?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=JP */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - rent?: { - /** @example /g8jqHtXJsMlc8B1Gb0Rt8AvUJMn.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 85 - */ - provider_id: number; - /** @example dTV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - KR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=KR */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /2ioan5BX5L9tz4fIGU93blTeFhv.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 356 - */ - provider_id: number; - /** @example wavve */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - KW?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=KW */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - LB?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LB */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - LI?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LI */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - LT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LT */ - link?: string; - rent?: { - /** @example /xTVM8uXT9QocigQ07LE7Irc65W2.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 553 - */ - provider_id: number; - /** @example Telia Play */ - provider_name?: string; - /** - * @default 0 - * @example 15 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - LV?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=LV */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - MD?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MD */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 26 - */ - display_priority: number; - }[]; - }; - MK?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MK */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - MT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MT */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - rent?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - MU?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MU */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 15 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 15 - */ - display_priority: number; - }[]; - }; - MX?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MX */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - MY?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MY */ - link?: string; - flatrate?: { - /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 122 - */ - provider_id: number; - /** @example Hotstar */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - MZ?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=MZ */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 16 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 16 - */ - display_priority: number; - }[]; - }; - NL?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NL */ - link?: string; - buy?: { - /** @example /llmnYOyknekZsXtkCaazKjhTLvG.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 71 - */ - provider_id: number; - /** @example Pathé Thuis */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - rent?: { - /** @example /llmnYOyknekZsXtkCaazKjhTLvG.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 71 - */ - provider_id: number; - /** @example Pathé Thuis */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - NO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NO */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - NZ?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=NZ */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - OM?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=OM */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PA */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PE */ - link?: string; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - PH?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PH */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PK?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PK */ - link?: string; - flatrate?: { - /** @example /t2yyOv40HZeVlLjYsCsPHnWLk4W.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 8 - */ - provider_id: number; - /** @example Netflix */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - PL?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PL */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PS?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PS */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PT */ - link?: string; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - rent?: { - /** @example /dUeHhim2WUZz8S7EWjv0Ws6anRP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 242 - */ - provider_id: number; - /** @example Meo */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - PY?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=PY */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - QA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=QA */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - RO?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RO */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - RS?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RS */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - RU?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=RU */ - link?: string; - rent?: { - /** @example /o9ExgOSLF3OTwR6T3DJOuwOKJgq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 113 - */ - provider_id: number; - /** @example Ivi */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - buy?: { - /** @example /o9ExgOSLF3OTwR6T3DJOuwOKJgq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 113 - */ - provider_id: number; - /** @example Ivi */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /zLM7f1w2L8TU2Fspzns72m6h3yY.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 501 - */ - provider_id: number; - /** @example Wink */ - provider_name?: string; - /** - * @default 0 - * @example 1000 - */ - display_priority: number; - }[]; - }; - SA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SA */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - SE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SE */ - link?: string; - buy?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - rent?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - SG?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SG */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - SI?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SI */ - link?: string; - buy?: { - /** @example /5GEbAhFW2S5T8zVc1MNvz00pIzM.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 35 - */ - provider_id: number; - /** @example Rakuten TV */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - SK?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SK */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - SM?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SM */ - link?: string; - flatrate?: { - /** @example /7rwgEs15tFwyR9NPQ5vpzxTj19Q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 337 - */ - provider_id: number; - /** @example Disney Plus */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - SV?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=SV */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - TH?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TH */ - link?: string; - flatrate?: { - /** @example /7Fl8ylPDclt3ZYgNbW2t7rbZE9I.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 122 - */ - provider_id: number; - /** @example Hotstar */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - TR?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TR */ - link?: string; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - TT?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TT */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - TW?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=TW */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - UG?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=UG */ - link?: string; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 16 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 16 - */ - display_priority: number; - }[]; - }; - US?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=US */ - link?: string; - rent?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /jPXksae158ukMLFhhlNvzsvaEyt.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 257 - */ - provider_id: number; - /** @example fuboTV */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - UY?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=UY */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - VE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=VE */ - link?: string; - rent?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - YE?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=YE */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - }; - ZA?: { - /** @example https://www.themoviedb.org/movie/550-fight-club/watch?locale=ZA */ - link?: string; - flatrate?: { - /** @example /emthp39XA2YScoYL1p0sdbAH2WA.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 119 - */ - provider_id: number; - /** @example Amazon Prime Video */ - provider_name?: string; - /** - * @default 0 - * @example 1 - */ - display_priority: number; - }[]; - rent?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - }; - }; - }; - }; - }; - }; - 'movie-add-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header: { - 'Content-Type': string; - }; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - }; - }; - 'movie-delete-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header?: { - 'Content-Type'?: string; - }; - path: { - movie_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 13 - */ - status_code: number; - /** @example The item/record was deleted successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'network-details': { - parameters: { - query?: never; - header?: never; - path: { - network_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example New York City, New York */ - headquarters?: string; - /** @example https://www.hbo.com */ - homepage?: string; - /** - * @default 0 - * @example 49 - */ - id: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - logo_path?: string; - /** @example HBO */ - name?: string; - /** @example US */ - origin_country?: string; - }; - }; - }; - }; - }; - 'details-copy': { - parameters: { - query?: never; - header?: never; - path: { - network_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 49 - */ - id: number; - results?: { - /** @example Home Box Office */ - name?: string; - /** @example */ - type?: string; - }[]; - }; - }; - }; - }; - }; - 'alternative-names-copy': { - parameters: { - query?: never; - header?: never; - path: { - network_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 49 - */ - id: number; - logos?: { - /** - * @default 0 - * @example 2.425287356321839 - */ - aspect_ratio: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - file_path?: string; - /** - * @default 0 - * @example 174 - */ - height: number; - /** @example 5a7a67a40e0a26020a000091 */ - id?: string; - /** @example .svg */ - file_type?: string; - /** - * @default 0 - * @example 5.318 - */ - vote_average: number; - /** - * @default 0 - * @example 3 - */ - vote_count: number; - /** - * @default 0 - * @example 422 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'person-popular-list': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 1 - */ - gender: number; - /** - * @default 0 - * @example 224513 - */ - id: number; - known_for?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /ilRyazdMJwN05exqhwK4tMKBYZs.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 335984 - */ - id: number; - /** @example movie */ - media_type?: string; - /** @example en */ - original_language?: string; - /** @example Blade Runner 2049 */ - original_title?: string; - /** @example Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years. */ - overview?: string; - /** @example /gajva2L0rPYkEWjzgFlBXCAVBE5.jpg */ - poster_path?: string; - /** @example 2017-10-04 */ - release_date?: string; - /** @example Blade Runner 2049 */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.5 - */ - vote_average: number; - /** - * @default 0 - * @example 11771 - */ - vote_count: number; - }[]; - /** @example Acting */ - known_for_department?: string; - /** @example Ana de Armas */ - name?: string; - /** - * @default 0 - * @example 343.33 - */ - popularity: number; - /** @example /3vxvsmYLTf4jnr163SUlBIw51ee.jpg */ - profile_path?: string; - }[]; - /** - * @default 0 - * @example 500 - */ - total_pages: number; - /** - * @default 0 - * @example 10000 - */ - total_results: number; - }; - }; - }; - }; - }; - 'person-details': { - parameters: { - query?: { - /** @description comma separated list of endpoints within this namespace, 20 items max */ - append_to_response?: string; - language?: string; - }; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - also_known_as?: string[]; - /** - * @example Thomas Jeffrey Hanks (born July 9, 1956) is an American actor and filmmaker. Known for both his comedic and dramatic roles, Hanks is one of the most popular and recognizable film stars worldwide, and is widely regarded as an American cultural icon. - * - * Hanks made his breakthrough with leading roles in the comedies Splash (1984) and Big (1988). He won two consecutive Academy Awards for Best Actor for starring as a gay lawyer suffering from AIDS in Philadelphia (1993) and a young man with below-average IQ in Forrest Gump (1994). Hanks collaborated with film director Steven Spielberg on five films: Saving Private Ryan (1998), Catch Me If You Can (2002), The Terminal (2004), Bridge of Spies (2015), and The Post (2017), as well as the 2001 miniseries Band of Brothers, which launched him as a director, producer, and screenwriter. - * - * Hanks' other notable films include the romantic comedies Sleepless in Seattle (1993) and You've Got Mail (1998); the dramas Apollo 13 (1995), The Green Mile (1999), Cast Away (2000), Road to Perdition (2002), and Cloud Atlas (2012); and the biographical dramas Saving Mr. Banks (2013), Captain Phillips (2013), Sully (2016), and A Beautiful Day in the Neighborhood (2019). He has also appeared as the title character in the Robert Langdon film series, and has voiced Sheriff Woody in the Toy Story film series. - * - * Description above from the Wikipedia article Tom Hanks, licensed under CC-BY-SA, full list of contributors on Wikipedia. - */ - biography?: string; - /** @example 1956-07-09 */ - birthday?: string; - deathday?: unknown; - /** - * @default 0 - * @example 2 - */ - gender: number; - homepage?: unknown; - /** - * @default 0 - * @example 31 - */ - id: number; - /** @example nm0000158 */ - imdb_id?: string; - /** @example Acting */ - known_for_department?: string; - /** @example Tom Hanks */ - name?: string; - /** @example Concord, California, USA */ - place_of_birth?: string; - /** - * @default 0 - * @example 82.989 - */ - popularity: number; - /** @example /xndWFsBlClOJFRdhSt4NBwiPq2o.jpg */ - profile_path?: string; - }; - }; - }; - }; - }; - 'person-changes': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - changes?: { - /** @example biography */ - key?: string; - items?: { - /** @example 640469b113654500ba4e859a */ - id?: string; - /** @example added */ - action?: string; - /** @example 2023-03-05 10:06:41 UTC */ - time?: string; - /** @example ca */ - iso_639_1?: string; - /** @example ES */ - iso_3166_1?: string; - /** - * @example Thomas "Tom" Jeffrey Hanks (Concord, Califòrnia, 9 de juliol de 1956) és un actor de cinema i productor estatunidenc, guanyador dues vegades de l'Oscar al millor actor i considerat un dels més versàtils i talentosos del cinema actual. - * - * Hanks és l'actor que més diners ha guanyat de tota la història del cinema amb un total de gairebé sis mil milions de dòlars (setembre 2006). És també copropietari de Playtone, una companyia de producció de pel·lícules. - */ - value?: string; - }[]; - }[]; - }; - }; - }; - }; - }; - 'person-combined-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - person_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 13 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Forrest Gump */ - original_title?: string; - /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ - overview?: string; - /** - * @default 0 - * @example 62.225 - */ - popularity: number; - /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ - poster_path?: string; - /** @example 1994-06-23 */ - release_date?: string; - /** @example Forrest Gump */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.481 - */ - vote_average: number; - /** - * @default 0 - * @example 24535 - */ - vote_count: number; - /** @example Forrest Gump */ - character?: string; - /** @example 52fe420ec3a36847f800074f */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - /** @example movie */ - media_type?: string; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /tx3uj8GPWf5pzb0gWATJ4bokNHI.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 87061 - */ - id: number; - /** @example fr */ - original_language?: string; - /** @example Le Voyage extraordinaire */ - original_title?: string; - /** @example An account of the extraordinary life of film pioneer Georges Méliès (1861-1938) and the amazing story of the copy in color of his masterpiece “A Trip to the Moon” (1902), unexpectedly found in Spain and restored thanks to the heroic efforts of a group of true cinema lovers. */ - overview?: string; - /** - * @default 0 - * @example 6.007 - */ - popularity: number; - /** @example /zHNNT9gfiGsuadR6x38KYOp6ekq.jpg */ - poster_path?: string; - /** @example 2011-12-08 */ - release_date?: string; - /** @example The Extraordinary Voyage */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.6 - */ - vote_average: number; - /** - * @default 0 - * @example 47 - */ - vote_count: number; - /** @example 5d818a63d34eb3002c4f8fea */ - credit_id?: string; - /** @example Crew */ - department?: string; - /** @example Thanks */ - job?: string; - /** @example movie */ - media_type?: string; - }[]; - /** - * @default 0 - * @example 31 - */ - id: number; - }; - }; - }; - }; - }; - 'person-external-ids': { - parameters: { - query?: never; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 31 - */ - id: number; - /** @example /m/0bxtg */ - freebase_mid?: string; - /** @example /en/tom_hanks */ - freebase_id?: string; - /** @example nm0000158 */ - imdb_id?: string; - /** - * @default 0 - * @example 14293 - */ - tvrage_id: number; - /** @example Q2263 */ - wikidata_id?: string; - /** @example TomHanks */ - facebook_id?: string; - /** @example tomhanks */ - instagram_id?: string; - /** @example tomhanks */ - tiktok_id?: string; - /** @example tomhanks */ - twitter_id?: string; - youtube_id?: unknown; - }; - }; - }; - }; - }; - 'person-images': { - parameters: { - query?: never; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 287 - */ - id: number; - profiles?: { - /** - * @default 0 - * @example 0.666 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 980 - */ - height: number; - iso_639_1?: unknown; - /** @example /cckcYc2v0yh1tc9QjRelptcOBko.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.288 - */ - vote_average: number; - /** - * @default 0 - * @example 89 - */ - vote_count: number; - /** - * @default 0 - * @example 653 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'person-latest-id': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - also_known_as?: unknown[]; - /** @example */ - biography?: string; - birthday?: unknown; - deathday?: unknown; - /** - * @default 0 - * @example 0 - */ - gender: number; - homepage?: unknown; - /** - * @default 0 - * @example 4064343 - */ - id: number; - imdb_id?: unknown; - known_for_department?: unknown; - /** @example Ángel Cruz */ - name?: string; - place_of_birth?: unknown; - /** - * @default 0 - * @example 0 - */ - popularity: number; - profile_path?: unknown; - }; - }; - }; - }; - }; - 'person-movie-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 13 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Forrest Gump */ - original_title?: string; - /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ - overview?: string; - /** - * @default 0 - * @example 62.225 - */ - popularity: number; - /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ - poster_path?: string; - /** @example 1994-06-23 */ - release_date?: string; - /** @example Forrest Gump */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.481 - */ - vote_average: number; - /** - * @default 0 - * @example 24535 - */ - vote_count: number; - /** @example Forrest Gump */ - character?: string; - /** @example 52fe420ec3a36847f800074f */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /tx3uj8GPWf5pzb0gWATJ4bokNHI.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 87061 - */ - id: number; - /** @example fr */ - original_language?: string; - /** @example Le Voyage extraordinaire */ - original_title?: string; - /** @example An account of the extraordinary life of film pioneer Georges Méliès (1861-1938) and the amazing story of the copy in color of his masterpiece “A Trip to the Moon” (1902), unexpectedly found in Spain and restored thanks to the heroic efforts of a group of true cinema lovers. */ - overview?: string; - /** - * @default 0 - * @example 6.007 - */ - popularity: number; - /** @example /zHNNT9gfiGsuadR6x38KYOp6ekq.jpg */ - poster_path?: string; - /** @example 2011-12-08 */ - release_date?: string; - /** @example The Extraordinary Voyage */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.6 - */ - vote_average: number; - /** - * @default 0 - * @example 47 - */ - vote_count: number; - /** @example 5d818a63d34eb3002c4f8fea */ - credit_id?: string; - /** @example Crew */ - department?: string; - /** @example Thanks */ - job?: string; - }[]; - /** - * @default 0 - * @example 31 - */ - id: number; - }; - }; - }; - }; - }; - 'person-tv-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /ttvojTMgaIN7U8gqB5LlNqO4vPN.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 1900 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example LIVE with Kelly and Mark */ - original_name?: string; - /** @example A morning talk show with A-list celebrity guests, top-notch performances and one-of-a-kind segments that are unrivaled on daytime television, plus spontaneous, hilarious and unpredictable talk. */ - overview?: string; - /** - * @default 0 - * @example 700.508 - */ - popularity: number; - /** @example /l5y8egG27p2fSTyq8s21SQMmQLy.jpg */ - poster_path?: string; - /** @example 1988-09-05 */ - first_air_date?: string; - /** @example LIVE with Kelly and Mark */ - name?: string; - /** - * @default 0 - * @example 5.4 - */ - vote_average: number; - /** - * @default 0 - * @example 25 - */ - vote_count: number; - /** @example */ - character?: string; - /** @example 52571af019c29571140d5c92 */ - credit_id?: string; - /** - * @default 0 - * @example 1 - */ - episode_count: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /6uMA6EAiwcsCqQJwWgYwtORvE0v.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 2391 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Tales from the Crypt */ - original_name?: string; - /** @example Cadaverous scream legend the Crypt Keeper is your macabre host for these forays of fright and fun based on the classic E.C. Comics tales from back in the day. So shamble up to the bar and pick your poison. Will it be an insane Santa on a personal slay ride? Honeymooners out to fulfill the "til death do we part" vow ASAP? */ - overview?: string; - /** - * @default 0 - * @example 24.88 - */ - popularity: number; - /** @example /dDfXQH6Kg2JNASI0dqNALukjhk1.jpg */ - poster_path?: string; - /** @example 1989-06-10 */ - first_air_date?: string; - /** @example Tales from the Crypt */ - name?: string; - /** - * @default 0 - * @example 7.978 - */ - vote_average: number; - /** - * @default 0 - * @example 757 - */ - vote_count: number; - /** @example 525734f3760ee3776a397211 */ - credit_id?: string; - /** @example Directing */ - department?: string; - /** - * @default 0 - * @example 1 - */ - episode_count: number; - /** @example Director */ - job?: string; - }[]; - /** - * @default 0 - * @example 31 - */ - id: number; - }; - }; - }; - }; - }; - 'person-tagged-images': { - parameters: { - query?: { - page?: number; - }; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 31 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default 0 - * @example 0.6666666666666666 - */ - aspect_ratio: number; - /** @example /1wY4psJ5NVEhCuOYROwLH2XExM2.jpg */ - file_path?: string; - /** - * @default 0 - * @example 1500 - */ - height: number; - /** @example 5b235d740e0a265b5d0031d9 */ - id?: string; - /** @example en */ - iso_639_1?: string; - /** - * @default 0 - * @example 5.456 - */ - vote_average: number; - /** - * @default 0 - * @example 7 - */ - vote_count: number; - /** - * @default 0 - * @example 1000 - */ - width: number; - /** @example poster */ - image_type?: string; - media?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /bdD39MpSVhKjxarTxLSfX6baoMP.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 857 - */ - id: number; - /** @example Saving Private Ryan */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Saving Private Ryan */ - original_title?: string; - /** @example As U.S. troops storm the beaches of Normandy, three brothers lie dead on the battlefield, with a fourth trapped behind enemy lines. Ranger captain John Miller and seven men are tasked with penetrating German-held territory and bringing the boy home. */ - overview?: string; - /** @example /uqx37cS8cpHg8U35f9U5IBlrCV3.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 70.45 - */ - popularity: number; - /** @example 1998-07-24 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.208 - */ - vote_average: number; - /** - * @default 0 - * @example 14134 - */ - vote_count: number; - }; - /** @example movie */ - media_type?: string; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 13 - */ - total_results: number; - }; - }; - }; - }; - }; - translations: { - parameters: { - query?: never; - header?: never; - path: { - person_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 31 - */ - id: number; - translations?: { - /** @example US */ - iso_3166_1?: string; - /** @example en */ - iso_639_1?: string; - /** @example English */ - name?: string; - /** @example English */ - english_name?: string; - data?: { - /** - * @example Thomas Jeffrey Hanks (born July 9, 1956) is an American actor and filmmaker. Known for both his comedic and dramatic roles, Hanks is one of the most popular and recognizable film stars worldwide, and is widely regarded as an American cultural icon. - * - * Hanks made his breakthrough with leading roles in the comedies Splash (1984) and Big (1988). He won two consecutive Academy Awards for Best Actor for starring as a gay lawyer suffering from AIDS in Philadelphia (1993) and a young man with below-average IQ in Forrest Gump (1994). Hanks collaborated with film director Steven Spielberg on five films: Saving Private Ryan (1998), Catch Me If You Can (2002), The Terminal (2004), Bridge of Spies (2015), and The Post (2017), as well as the 2001 miniseries Band of Brothers, which launched him as a director, producer, and screenwriter. - * - * Hanks' other notable films include the romantic comedies Sleepless in Seattle (1993) and You've Got Mail (1998); the dramas Apollo 13 (1995), The Green Mile (1999), Cast Away (2000), Road to Perdition (2002), and Cloud Atlas (2012); and the biographical dramas Saving Mr. Banks (2013), Captain Phillips (2013), Sully (2016), and A Beautiful Day in the Neighborhood (2019). He has also appeared as the title character in the Robert Langdon film series, and has voiced Sheriff Woody in the Toy Story film series. - * - * Description above from the Wikipedia article Tom Hanks, licensed under CC-BY-SA, full list of contributors on Wikipedia. - */ - biography?: string; - /** @example Tom Hanks */ - name?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'review-details': { - parameters: { - query?: never; - header?: never; - path: { - review_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example 640b2aeecaaca20079decdcc */ - id?: string; - /** @example Ricardo Oliveira */ - author?: string; - author_details?: { - /** @example Ricardo Oliveira */ - name?: string; - /** @example RSOliveira */ - username?: string; - /** @example /23Cl7rhsknc7IIAcZZAGKzovjTu.jpg */ - avatar_path?: string; - /** - * @default 0 - * @example 9 - */ - rating: number; - }; - /** - * @example "The Last of Us" is a post-apocalyptic TV series based on the popular video game of the same name. The story follows the journey of Joel, a smuggler, and Ellie, a teenage girl who may be the key to finding a cure for a deadly fungal infection that has ravaged the world. - * - * The series features outstanding performances from Pedro Pascal as Joel, Bella Ramsey as Ellie, and Anna Torv as Tess. The chemistry between the main characters is excellent, and the casting is spot-on. - * - * The show's writing is superb, and it captures the essence of the video game while adding a fresh perspective. The narrative is engaging, and the pacing is just right, with each episode leaving you on the edge of your seat, eager to see what happens next. - * - * The show's production value is top-notch, with stunning visuals and cinematography that capture the bleak and haunting atmosphere of a post-apocalyptic world. The use of practical effects and makeup is impressive and adds to the overall immersion of the story. - * - * Overall, "The Last of Us" is an outstanding TV series that does justice to the source material. It's a must-watch for fans of the video game and anyone who enjoys gripping and emotional storytelling. I would rate it a 9 out of 10. - * - * - * - * Written and Reviewed by RSOliveira - */ - content?: string; - /** @example 2023-03-10T13:04:46.674Z */ - created_at?: string; - /** @example en */ - iso_639_1?: string; - /** - * @default 0 - * @example 100088 - */ - media_id: number; - /** @example The Last of Us */ - media_title?: string; - /** @example tv */ - media_type?: string; - /** @example 2023-03-10T13:04:46.734Z */ - updated_at?: string; - /** @example https://www.themoviedb.org/review/640b2aeecaaca20079decdcc */ - url?: string; - }; - }; - }; - }; - }; - 'search-collection': { - parameters: { - query: { - query: string; - include_adult?: boolean; - language?: string; - page?: number; - region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /zuW6fOiusv4X9nnW3paHGfXcSll.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 86311 - */ - id: number; - /** @example The Avengers Collection */ - name?: string; - /** @example en */ - original_language?: string; - /** @example The Avengers Collection */ - original_name?: string; - /** @example A superhero film series produced by Marvel Studios based on the Marvel Comics superhero team of the same name, and part of the Marvel Cinematic Universe (MCU). The series features an ensemble cast from the Marvel Cinematic Universe series films, as they join forces for the peacekeeping organization S.H.I.E.L.D. led by Nick Fury. */ - overview?: string; - /** @example /yFSIUVTCvgYrpalUktulvk3Gi5Y.jpg */ - poster_path?: string; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-company': { - parameters: { - query: { - query: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default 0 - * @example 3268 - */ - id: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - logo_path?: string; - /** @example HBO */ - name?: string; - /** @example US */ - origin_country?: string; - }[]; - /** - * @default 0 - * @example 2 - */ - total_pages: number; - /** - * @default 0 - * @example 22 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-keyword': { - parameters: { - query: { - query: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default 0 - * @example 262419 - */ - id: number; - /** @example lost */ - name?: string; - }[]; - /** - * @default 0 - * @example 5 - */ - total_pages: number; - /** - * @default 0 - * @example 84 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-movie': { - parameters: { - query: { - query: string; - include_adult?: boolean; - language?: string; - primary_release_year?: string; - page?: number; - region?: string; - year?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 550 - */ - id: number; - /** @example en */ - original_language?: string; - /** @example Fight Club */ - original_title?: string; - /** @example A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion. */ - overview?: string; - /** - * @default 0 - * @example 73.433 - */ - popularity: number; - /** @example /pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg */ - poster_path?: string; - /** @example 1999-10-15 */ - release_date?: string; - /** @example Fight Club */ - title?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.433 - */ - vote_average: number; - /** - * @default 0 - * @example 26279 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 2 - */ - total_pages: number; - /** - * @default 0 - * @example 39 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-multi': { - parameters: { - query: { - query: string; - include_adult?: boolean; - language?: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /aDYSnJAK0BTVeE8osOy22Kz3SXY.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 11 - */ - id: number; - /** @example Star Wars */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Star Wars */ - original_title?: string; - /** @example Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire. */ - overview?: string; - /** @example /6FfCtAuVAW8XJjZ7eWeLibRLWTw.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 78.047 - */ - popularity: number; - /** @example 1977-05-25 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.208 - */ - vote_average: number; - /** - * @default 0 - * @example 18528 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 11 - */ - total_pages: number; - /** - * @default 0 - * @example 201 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-person': { - parameters: { - query: { - query: string; - include_adult?: boolean; - language?: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 31 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Tom Hanks */ - name?: string; - /** @example Tom Hanks */ - original_name?: string; - /** - * @default 0 - * @example 84.631 - */ - popularity: number; - /** @example /xndWFsBlClOJFRdhSt4NBwiPq2o.jpg */ - profile_path?: string; - known_for?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 13 - */ - id: number; - /** @example Forrest Gump */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Forrest Gump */ - original_title?: string; - /** @example A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him. */ - overview?: string; - /** @example /arw2vcBveWOVZr6pxd9XTd1TdQa.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 67.209 - */ - popularity: number; - /** @example 1994-06-23 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 8.481 - */ - vote_average: number; - /** - * @default 0 - * @example 24525 - */ - vote_count: number; - }[]; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'search-tv': { - parameters: { - query: { - query: string; - /** @description Search only the first air date. Valid values are: 1000..9999 */ - first_air_date_year?: number; - include_adult?: boolean; - language?: string; - page?: number; - /** @description Search the first air date and all episode air dates. Valid values are: 1000..9999 */ - year?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 1396 - */ - id: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Breaking Bad */ - original_name?: string; - /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ - overview?: string; - /** - * @default 0 - * @example 298.884 - */ - popularity: number; - /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ - poster_path?: string; - /** @example 2008-01-20 */ - first_air_date?: string; - /** @example Breaking Bad */ - name?: string; - /** - * @default 0 - * @example 8.879 - */ - vote_average: number; - /** - * @default 0 - * @example 11536 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 1 - */ - total_results: number; - }; - }; - }; - }; - }; - 'trending-all': { - parameters: { - query?: { - /** @description `ISO-639-1`-`ISO-3166-1` code */ - language?: string; - }; - header?: never; - path: { - time_window: 'day' | 'week'; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 934433 - */ - id: number; - /** @example Scream VI */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Scream VI */ - original_title?: string; - /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ - overview?: string; - /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 609.941 - */ - popularity: number; - /** @example 2023-03-08 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.374 - */ - vote_average: number; - /** - * @default 0 - * @example 684 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 1000 - */ - total_pages: number; - /** - * @default 0 - * @example 20000 - */ - total_results: number; - }; - }; - }; - }; - }; - 'trending-movies': { - parameters: { - query?: { - /** @description `ISO-639-1`-`ISO-3166-1` code */ - language?: string; - }; - header?: never; - path: { - time_window: 'day' | 'week'; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /44immBwzhDVyjn87b3x3l9mlhAD.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 934433 - */ - id: number; - /** @example Scream VI */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Scream VI */ - original_title?: string; - /** @example Following the latest Ghostface killings, the four survivors leave Woodsboro behind and start a fresh chapter. */ - overview?: string; - /** @example /wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 609.941 - */ - popularity: number; - /** @example 2023-03-08 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.374 - */ - vote_average: number; - /** - * @default 0 - * @example 684 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 1000 - */ - total_pages: number; - /** - * @default 0 - * @example 20000 - */ - total_results: number; - }; - }; - }; - }; - }; - 'trending-people': { - parameters: { - query?: { - /** @description `ISO-639-1`-`ISO-3166-1` code */ - language?: string; - }; - header?: never; - path: { - time_window: 'day' | 'week'; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 224513 - */ - id: number; - /** @example Ana de Armas */ - name?: string; - /** @example Ana de Armas */ - original_name?: string; - /** @example person */ - media_type?: string; - /** - * @default 0 - * @example 349.766 - */ - popularity: number; - /** - * @default 0 - * @example 1 - */ - gender: number; - /** @example Acting */ - known_for_department?: string; - /** @example /3vxvsmYLTf4jnr163SUlBIw51ee.jpg */ - profile_path?: string; - known_for?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /ilRyazdMJwN05exqhwK4tMKBYZs.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 335984 - */ - id: number; - /** @example Blade Runner 2049 */ - title?: string; - /** @example en */ - original_language?: string; - /** @example Blade Runner 2049 */ - original_title?: string; - /** @example Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years. */ - overview?: string; - /** @example /gajva2L0rPYkEWjzgFlBXCAVBE5.jpg */ - poster_path?: string; - /** @example movie */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 79.571 - */ - popularity: number; - /** @example 2017-10-04 */ - release_date?: string; - /** - * @default true - * @example false - */ - video: boolean; - /** - * @default 0 - * @example 7.531 - */ - vote_average: number; - /** - * @default 0 - * @example 11771 - */ - vote_count: number; - }[]; - }[]; - /** - * @default 0 - * @example 1000 - */ - total_pages: number; - /** - * @default 0 - * @example 20000 - */ - total_results: number; - }; - }; - }; - }; - }; - 'trending-tv': { - parameters: { - query?: { - /** @description `ISO-639-1`-`ISO-3166-1` code */ - language?: string; - }; - header?: never; - path: { - time_window: 'day' | 'week'; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /8P15FsYcTwQZ4G5rRMd1TKD14Aq.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 103768 - */ - id: number; - /** @example Sweet Tooth */ - name?: string; - /** @example en */ - original_language?: string; - /** @example Sweet Tooth */ - original_name?: string; - /** @example On a perilous adventure across a post-apocalyptic world, a lovable boy who's half-human and half-deer searches for a new beginning with a gruff protector. */ - overview?: string; - /** @example /dBxxtfhC4vYrxB2fLsSxOTY2dQc.jpg */ - poster_path?: string; - /** @example tv */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 137.498 - */ - popularity: number; - /** @example 2021-06-04 */ - first_air_date?: string; - /** - * @default 0 - * @example 7.928 - */ - vote_average: number; - /** - * @default 0 - * @example 1094 - */ - vote_count: number; - origin_country?: string[]; - }[]; - /** - * @default 0 - * @example 1000 - */ - total_pages: number; - /** - * @default 0 - * @example 20000 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-airing-today-list': { - parameters: { - query?: { - language?: string; - page?: number; - timezone?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ - backdrop_path?: string; - /** @example 2023-01-23 */ - first_air_date?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 202250 - */ - id: number; - /** @example Dirty Linen */ - name?: string; - origin_country?: string[]; - /** @example tl */ - original_language?: string; - /** @example Dirty Linen */ - original_name?: string; - /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ - overview?: string; - /** - * @default 0 - * @example 2797.914 - */ - popularity: number; - /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 5 - */ - vote_average: number; - /** - * @default 0 - * @example 13 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 14 - */ - total_pages: number; - /** - * @default 0 - * @example 265 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-on-the-air-list': { - parameters: { - query?: { - language?: string; - page?: number; - timezone?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ - backdrop_path?: string; - /** @example 2023-01-23 */ - first_air_date?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 202250 - */ - id: number; - /** @example Dirty Linen */ - name?: string; - origin_country?: string[]; - /** @example tl */ - original_language?: string; - /** @example Dirty Linen */ - original_name?: string; - /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ - overview?: string; - /** - * @default 0 - * @example 2797.914 - */ - popularity: number; - /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 5 - */ - vote_average: number; - /** - * @default 0 - * @example 13 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 58 - */ - total_pages: number; - /** - * @default 0 - * @example 1151 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-popular-list': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example /mAJ84W6I8I272Da87qplS2Dp9ST.jpg */ - backdrop_path?: string; - /** @example 2023-01-23 */ - first_air_date?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 202250 - */ - id: number; - /** @example Dirty Linen */ - name?: string; - origin_country?: string[]; - /** @example tl */ - original_language?: string; - /** @example Dirty Linen */ - original_name?: string; - /** @example To exact vengeance, a young woman infiltrates the household of an influential family as a housemaid to expose their dirty secrets. However, love will get in the way of her revenge plot. */ - overview?: string; - /** - * @default 0 - * @example 2797.914 - */ - popularity: number; - /** @example /aoAZgnmMzY9vVy9VWnO3U5PZENh.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 5 - */ - vote_average: number; - /** - * @default 0 - * @example 13 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 7416 - */ - total_pages: number; - /** - * @default 0 - * @example 148302 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-top-rated-list': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example /99vBORZixICa32Pwdwj0lWcr8K.jpg */ - backdrop_path?: string; - /** @example 2021-09-03 */ - first_air_date?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 130392 - */ - id: number; - /** @example The D'Amelio Show */ - name?: string; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example The D'Amelio Show */ - original_name?: string; - /** @example From relative obscurity and a seemingly normal life, to overnight success and thrust into the Hollywood limelight overnight, the D’Amelios are faced with new challenges and opportunities they could not have imagined. */ - overview?: string; - /** - * @default 0 - * @example 12.459 - */ - popularity: number; - /** @example /phv2Jc4H8cvRzvTKb9X1uKMboTu.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 8.9 - */ - vote_average: number; - /** - * @default 0 - * @example 3190 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 142 - */ - total_pages: number; - /** - * @default 0 - * @example 2833 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-details': { - parameters: { - query?: { - /** @description comma separated list of endpoints within this namespace, 20 items max */ - append_to_response?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /6LWy0jvMpmjoS9fojNgHIKoWL05.jpg */ - backdrop_path?: string; - created_by?: { - /** - * @default 0 - * @example 9813 - */ - id: number; - /** @example 5256c8c219c2956ff604858a */ - credit_id?: string; - /** @example David Benioff */ - name?: string; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** @example /xvNN5huL0X8yJ7h3IZfGG4O2zBD.jpg */ - profile_path?: string; - }[]; - episode_run_time?: number[]; - /** @example 2011-04-17 */ - first_air_date?: string; - genres?: { - /** - * @default 0 - * @example 10765 - */ - id: number; - /** @example Sci-Fi & Fantasy */ - name?: string; - }[]; - /** @example http://www.hbo.com/game-of-thrones */ - homepage?: string; - /** - * @default 0 - * @example 1399 - */ - id: number; - /** - * @default true - * @example false - */ - in_production: boolean; - languages?: string[]; - /** @example 2019-05-19 */ - last_air_date?: string; - last_episode_to_air?: { - /** - * @default 0 - * @example 1551830 - */ - id: number; - /** @example The Iron Throne */ - name?: string; - /** @example In the aftermath of the devastating attack on King's Landing, Daenerys must face the survivors. */ - overview?: string; - /** - * @default 0 - * @example 4.809 - */ - vote_average: number; - /** - * @default 0 - * @example 241 - */ - vote_count: number; - /** @example 2019-05-19 */ - air_date?: string; - /** - * @default 0 - * @example 6 - */ - episode_number: number; - /** @example 806 */ - production_code?: string; - /** - * @default 0 - * @example 80 - */ - runtime: number; - /** - * @default 0 - * @example 8 - */ - season_number: number; - /** - * @default 0 - * @example 1399 - */ - show_id: number; - /** @example /zBi2O5EJfgTS6Ae0HdAYLm9o2nf.jpg */ - still_path?: string; - }; - /** @example Game of Thrones */ - name?: string; - next_episode_to_air?: unknown; - networks?: { - /** - * @default 0 - * @example 49 - */ - id: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - logo_path?: string; - /** @example HBO */ - name?: string; - /** @example US */ - origin_country?: string; - }[]; - /** - * @default 0 - * @example 73 - */ - number_of_episodes: number; - /** - * @default 0 - * @example 8 - */ - number_of_seasons: number; - origin_country?: string[]; - /** @example en */ - original_language?: string; - /** @example Game of Thrones */ - original_name?: string; - /** @example Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond. */ - overview?: string; - /** - * @default 0 - * @example 346.098 - */ - popularity: number; - /** @example /1XS1oqL89opfnbLl8WnZY1O1uJx.jpg */ - poster_path?: string; - production_companies?: { - /** - * @default 0 - * @example 76043 - */ - id: number; - /** @example /9RO2vbQ67otPrBLXCaC8UMp3Qat.png */ - logo_path?: string; - /** @example Revolution Sun Studios */ - name?: string; - /** @example US */ - origin_country?: string; - }[]; - production_countries?: { - /** @example GB */ - iso_3166_1?: string; - /** @example United Kingdom */ - name?: string; - }[]; - seasons?: { - /** @example 2010-12-05 */ - air_date?: string; - /** - * @default 0 - * @example 272 - */ - episode_count: number; - /** - * @default 0 - * @example 3627 - */ - id: number; - /** @example Specials */ - name?: string; - /** @example */ - overview?: string; - /** @example /kMTcwNRfFKCZ0O2OaBZS0nZ2AIe.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 0 - */ - season_number: number; - /** - * @default 0 - * @example 0 - */ - vote_average: number; - }[]; - spoken_languages?: { - /** @example English */ - english_name?: string; - /** @example en */ - iso_639_1?: string; - /** @example English */ - name?: string; - }[]; - /** @example Ended */ - status?: string; - /** @example Winter Is Coming */ - tagline?: string; - /** @example Scripted */ - type?: string; - /** - * @default 0 - * @example 8.438 - */ - vote_average: number; - /** - * @default 0 - * @example 21390 - */ - vote_count: number; - }; - }; - }; - }; - }; - 'tv-series-account-states': { - parameters: { - query?: { - session_id?: string; - guest_session_id?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** - * @default true - * @example true - */ - favorite: boolean; - rated?: { - /** - * @default 0 - * @example 9 - */ - value: number; - }; - /** - * @default true - * @example false - */ - watchlist: boolean; - }; - }; - }; - }; - }; - 'tv-series-aggregate-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 1 - */ - gender: number; - /** - * @default 0 - * @example 1223786 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Emilia Clarke */ - name?: string; - /** @example Emilia Clarke */ - original_name?: string; - /** - * @default 0 - * @example 42.737 - */ - popularity: number; - /** @example /u59kTmNHXzaGZqokivxLPiBVIML.jpg */ - profile_path?: string; - roles?: { - /** @example 5256c8af19c2956ff60479f6 */ - credit_id?: string; - /** @example Daenerys Targaryen */ - character?: string; - /** - * @default 0 - * @example 78 - */ - episode_count: number; - }[]; - /** - * @default 0 - * @example 78 - */ - total_episode_count: number; - /** - * @default 0 - * @example 6 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 1 - */ - gender: number; - /** - * @default 0 - * @example 6411 - */ - id: number; - /** @example Art */ - known_for_department?: string; - /** @example Deborah Riley */ - name?: string; - /** @example Deborah Riley */ - original_name?: string; - /** - * @default 0 - * @example 1.4 - */ - popularity: number; - /** @example /cjhADpqdrnwB1PdDUKaBnWrIj2Q.jpg */ - profile_path?: string; - jobs?: { - /** @example 54eee9e5c3a3686d5800584e */ - credit_id?: string; - /** @example Production Design */ - job?: string; - /** - * @default 0 - * @example 43 - */ - episode_count: number; - }[]; - /** @example Art */ - department?: string; - /** - * @default 0 - * @example 43 - */ - total_episode_count: number; - }[]; - /** - * @default 0 - * @example 1399 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-series-alternative-titles': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - results?: { - /** @example AL */ - iso_3166_1?: string; - /** @example Froni i shpatave */ - title?: string; - /** @example */ - type?: string; - }[]; - }; - }; - }; - }; - }; - 'tv-series-changes': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - changes?: { - /** @example images */ - key?: string; - items?: { - /** @example 640435cf021cee0084710972 */ - id?: string; - /** @example updated */ - action?: string; - /** @example 2023-03-05 06:25:19 UTC */ - time?: string; - /** @example en */ - iso_639_1?: string; - /** @example */ - iso_3166_1?: string; - value?: { - poster?: { - /** @example /ouudK6RCNnsbT1CSXrlATXQIQTG.jpg */ - file_path?: string; - /** @example en */ - iso_639_1?: string; - }; - }; - original_value?: { - poster?: { - /** @example /ouudK6RCNnsbT1CSXrlATXQIQTG.jpg */ - file_path?: string; - /** @example fr */ - iso_639_1?: string; - }; - }; - }[]; - }[]; - }; - }; - }; - }; - }; - 'tv-series-content-ratings': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - descriptors?: unknown[]; - /** @example DE */ - iso_3166_1?: string; - /** @example 16 */ - rating?: string; - }[]; - /** - * @default 0 - * @example 1399 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-series-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 22970 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Peter Dinklage */ - name?: string; - /** @example Peter Dinklage */ - original_name?: string; - /** - * @default 0 - * @example 30.6 - */ - popularity: number; - /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ - profile_path?: string; - /** @example Tyrion Lannister */ - character?: string; - /** @example 5256c8b219c2956ff6047cd8 */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 1406855 - */ - id: number; - /** @example Production */ - known_for_department?: string; - /** @example Duncan Muggoch */ - name?: string; - /** @example Duncan Muggoch */ - original_name?: string; - /** - * @default 0 - * @example 1.592 - */ - popularity: number; - /** @example /ukGjJ62Ejd4cFziald03G34Fsrp.jpg */ - profile_path?: string; - /** @example 5ceab029c3a3682e93217a85 */ - credit_id?: string; - /** @example Production */ - department?: string; - /** @example Producer */ - job?: string; - }[]; - /** - * @default 0 - * @example 1399 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-series-episode-groups': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - /** @example */ - description?: string; - /** - * @default 0 - * @example 102 - */ - episode_count: number; - /** - * @default 0 - * @example 9 - */ - group_count: number; - /** @example 5e9077d2e640d600151f32bd */ - id?: string; - /** @example Aired Order */ - name?: string; - network?: { - /** - * @default 0 - * @example 49 - */ - id: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - logo_path?: string; - /** @example HBO */ - name?: string; - /** @example US */ - origin_country?: string; - }; - /** - * @default 0 - * @example 1 - */ - type: number; - }[]; - /** - * @default 0 - * @example 1399 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-series-external-ids': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - /** @example tt0944947 */ - imdb_id?: string; - /** @example /m/0524b41 */ - freebase_mid?: string; - /** @example /en/game_of_thrones */ - freebase_id?: string; - /** - * @default 0 - * @example 121361 - */ - tvdb_id: number; - /** - * @default 0 - * @example 24493 - */ - tvrage_id: number; - /** @example Q23572 */ - wikidata_id?: string; - /** @example GameOfThrones */ - facebook_id?: string; - /** @example gameofthrones */ - instagram_id?: string; - /** @example GameOfThrones */ - twitter_id?: string; - }; - }; - }; - }; - }; - 'tv-series-images': { - parameters: { - query?: { - /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ - include_image_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - backdrops?: { - /** - * @default 0 - * @example 1.778 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 800 - */ - height: number; - iso_639_1?: unknown; - /** @example /hZkgoQYus5vegHoetLkCJzb17zJ.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.622 - */ - vote_average: number; - /** - * @default 0 - * @example 20 - */ - vote_count: number; - /** - * @default 0 - * @example 1422 - */ - width: number; - }[]; - /** - * @default 0 - * @example 550 - */ - id: number; - logos?: { - /** - * @default 0 - * @example 5.203 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 79 - */ - height: number; - /** @example he */ - iso_639_1?: string; - /** @example /c1KLulrIhUqY5fT42nmC5aERGCp.png */ - file_path?: string; - /** - * @default 0 - * @example 5.312 - */ - vote_average: number; - /** - * @default 0 - * @example 1 - */ - vote_count: number; - /** - * @default 0 - * @example 411 - */ - width: number; - }[]; - posters?: { - /** - * @default 0 - * @example 0.667 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 900 - */ - height: number; - /** @example pt */ - iso_639_1?: string; - /** @example /r3pPehX4ik8NLYPpbDRAh0YRtMb.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.258 - */ - vote_average: number; - /** - * @default 0 - * @example 6 - */ - vote_count: number; - /** - * @default 0 - * @example 600 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'tv-series-keywords': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - results?: { - /** @example based on novel or book */ - name?: string; - /** - * @default 0 - * @example 818 - */ - id: number; - }[]; - }; - }; - }; - }; - }; - 'tv-series-latest-id': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default true - * @example false - */ - adult: boolean; - backdrop_path?: unknown; - created_by?: unknown[]; - episode_run_time?: unknown[]; - /** @example */ - first_air_date?: string; - genres?: unknown[]; - /** @example */ - homepage?: string; - /** - * @default 0 - * @example 225491 - */ - id: number; - /** - * @default true - * @example true - */ - in_production: boolean; - languages?: unknown[]; - /** @example 2023-04-21 */ - last_air_date?: string; - last_episode_to_air?: { - /** - * @default 0 - * @example 4398801 - */ - id: number; - /** @example Episode 8 */ - name?: string; - /** @example */ - overview?: string; - /** - * @default 0 - * @example 0 - */ - vote_average: number; - /** - * @default 0 - * @example 0 - */ - vote_count: number; - /** @example 2023-04-21 */ - air_date?: string; - /** - * @default 0 - * @example 8 - */ - episode_number: number; - /** @example */ - production_code?: string; - runtime?: unknown; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** - * @default 0 - * @example 225491 - */ - show_id: number; - still_path?: unknown; - }; - /** @example 妖怪传 */ - name?: string; - next_episode_to_air?: unknown; - networks?: unknown[]; - /** - * @default 0 - * @example 1 - */ - number_of_episodes: number; - /** - * @default 0 - * @example 1 - */ - number_of_seasons: number; - origin_country?: string[]; - /** @example zh */ - original_language?: string; - /** @example 妖怪传 */ - original_name?: string; - /** @example */ - overview?: string; - /** - * @default 0 - * @example 0 - */ - popularity: number; - poster_path?: unknown; - production_companies?: unknown[]; - production_countries?: unknown[]; - seasons?: { - air_date?: unknown; - /** - * @default 0 - * @example 1 - */ - episode_count: number; - /** - * @default 0 - * @example 338956 - */ - id: number; - /** @example Season 1 */ - name?: string; - /** @example */ - overview?: string; - poster_path?: unknown; - /** - * @default 0 - * @example 1 - */ - season_number: number; - }[]; - spoken_languages?: unknown[]; - /** @example Returning Series */ - status?: string; - /** @example */ - tagline?: string; - /** @example Scripted */ - type?: string; - /** - * @default 0 - * @example 0 - */ - vote_average: number; - /** - * @default 0 - * @example 0 - */ - vote_count: number; - }; - }; - }; - }; - }; - 'lists-copy': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example */ - description?: string; - /** - * @default 0 - * @example 0 - */ - favorite_count: number; - /** - * @default 0 - * @example 8257231 - */ - id: number; - /** - * @default 0 - * @example 182 - */ - item_count: number; - /** @example en */ - iso_639_1?: string; - /** @example US */ - iso_3166_1?: string; - /** @example Done */ - name?: string; - poster_path?: unknown; - }[]; - /** - * @default 0 - * @example 96 - */ - total_pages: number; - /** - * @default 0 - * @example 1906 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-recommendations': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ - backdrop_path?: string; - /** - * @default 0 - * @example 1396 - */ - id: number; - /** @example Breaking Bad */ - name?: string; - /** @example en */ - original_language?: string; - /** @example Breaking Bad */ - original_name?: string; - /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ - overview?: string; - /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ - poster_path?: string; - /** @example tv */ - media_type?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 292.904 - */ - popularity: number; - /** @example 2008-01-20 */ - first_air_date?: string; - /** - * @default 0 - * @example 8.878 - */ - vote_average: number; - /** - * @default 0 - * @example 11544 - */ - vote_count: number; - origin_country?: string[]; - }[]; - /** - * @default 0 - * @example 2 - */ - total_pages: number; - /** - * @default 0 - * @example 40 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-reviews': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** @example lmao7 */ - author?: string; - author_details?: { - /** @example lmao7 */ - name?: string; - /** @example lmao7 */ - username?: string; - /** @example /ekmYOUU4tfx9zGGadjRdE7UPce.jpg */ - avatar_path?: string; - /** - * @default 0 - * @example 9 - */ - rating: number; - }; - /** - * @example I started watching when it came out as I heard that fans of LOTR also liked this. I stopped watching after Season 1 as I was devastated lol kinda. Only 2015 I decided to continue watching and got addicted like it seemed complicated at first, too many stories and characters. I even used a guide from internet like family tree per house while watching or GOT wiki so I can have more background on the characters. For a TV series, this show can really take you to a different world and never knowing what will happen. It is very daring that any time anybody can just die (I learned not to be attached and have accepted that they will all die so I won't be devastated hehe). I have never read the books but the show is entertaining and you will really root for your faves and really hate on those you hate. - * - * Fantasy, action, drama, comedy, love...and lots of surprises! - */ - content?: string; - /** @example 2017-02-20T05:47:28.872Z */ - created_at?: string; - /** @example 58aa82f09251416f92006a3a */ - id?: string; - /** @example 2021-06-23T15:57:54.649Z */ - updated_at?: string; - /** @example https://www.themoviedb.org/review/58aa82f09251416f92006a3a */ - url?: string; - }[]; - /** - * @default 0 - * @example 1 - */ - total_pages: number; - /** - * @default 0 - * @example 11 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-screened-theatrically': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - results?: { - /** - * @default 0 - * @example 1159054 - */ - id: number; - /** - * @default 0 - * @example 10 - */ - episode_number: number; - /** - * @default 0 - * @example 5 - */ - season_number: number; - }[]; - }; - }; - }; - }; - }; - 'tv-series-similar': { - parameters: { - query?: { - language?: string; - page?: number; - }; - header?: never; - path: { - series_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - page: number; - results?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** @example /zcFSvWa34nDn2NcqOPuthyOIBWT.jpg */ - backdrop_path?: string; - genre_ids?: number[]; - /** - * @default 0 - * @example 197063 - */ - id: number; - origin_country?: string[]; - /** @example ko */ - original_language?: string; - /** @example 종이달 */ - original_name?: string; - /** @example A thriller drama about Yoo I-hwa, a stay-at-home mom living her comfortable and contented life without desires, but to her husband's indifference. While working as a bank contract employee, she unexpectedly touches money from VIP clients and gradually falls into an irreversible collapse. */ - overview?: string; - /** - * @default 0 - * @example 12.299 - */ - popularity: number; - /** @example /xXWynVdMGyJXBUDvIN27AXM3iJJ.jpg */ - poster_path?: string; - /** @example 2023-04-10 */ - first_air_date?: string; - /** @example Pale Moon */ - name?: string; - /** - * @default 0 - * @example 7 - */ - vote_average: number; - /** - * @default 0 - * @example 2 - */ - vote_count: number; - }[]; - /** - * @default 0 - * @example 82 - */ - total_pages: number; - /** - * @default 0 - * @example 1639 - */ - total_results: number; - }; - }; - }; - }; - }; - 'tv-series-translations': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - translations?: { - /** @example SA */ - iso_3166_1?: string; - /** @example ar */ - iso_639_1?: string; - /** @example العربية */ - name?: string; - /** @example Arabic */ - english_name?: string; - data?: { - /** @example صراع العروش */ - name?: string; - /** @example تتقاتل سبع عائلات نبيلة من أجل السيطرة على أرض - ويستيروس - الأسطورية. الاحتكاك بين العوائل يؤدي إلى حرب واسعة النطاق. في حين يستيقظ الشر القديم في أقصى الشمال. وفي خضم الحرب، نظام عسكري مهمَل - حرس الليل - هم كل ما يقف بين عالم الإنسان والأهوال الجليدية. */ - overview?: string; - /** @example */ - homepage?: string; - /** @example الشتاء قادم */ - tagline?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'tv-series-videos': { - parameters: { - query?: { - /** @description filter the list results by language, supports more than one value by using a comma */ - include_video_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - results?: { - /** @example en */ - iso_639_1?: string; - /** @example US */ - iso_3166_1?: string; - /** @example Inside Game of Thrones: A Story in Camera Work – BTS (HBO) */ - name?: string; - /** @example y2ZJ3lTaREY */ - key?: string; - /** @example YouTube */ - site?: string; - /** - * @default 0 - * @example 1080 - */ - size: number; - /** @example Behind the Scenes */ - type?: string; - /** - * @default true - * @example true - */ - official: boolean; - /** @example 2019-03-25T14:00:06.000Z */ - published_at?: string; - /** @example 5c999b48c3a36863b73b9d42 */ - id?: string; - }[]; - }; - }; - }; - }; - }; - 'tv-series-watch-providers': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1399 - */ - id: number; - results?: { - AE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AE */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - AR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AR */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - AT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AT */ - link?: string; - buy?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /y0kyIFElN5sJAsmW8Txj69wzrD2.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 321 - */ - provider_id: number; - /** @example Sky X */ - provider_name?: string; - /** - * @default 0 - * @example 23 - */ - display_priority: number; - }[]; - }; - AU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AU */ - link?: string; - flatrate?: { - /** @example /d3ixI1no0EpTj2i7u0Sd2DBXVlG.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 385 - */ - provider_id: number; - /** @example BINGE */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - BA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BA */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - BB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BB */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - BE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BE */ - link?: string; - flatrate?: { - /** @example /pq8p1umEnJjdFAP1nFvNArTR61X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 311 - */ - provider_id: number; - /** @example Be TV Go */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - BG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BG */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 15 - */ - display_priority: number; - }[]; - }; - BO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BO */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - BR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BR */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - BS?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BS */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - CA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CA */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /gJ3yVMWouaVj6iHd59TISJ1TlM5.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 230 - */ - provider_id: number; - /** @example Crave */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - CH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CH */ - link?: string; - flatrate?: { - /** @example /sHP8XLo4Ac4WMbziRyAdRQdb76q.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 210 - */ - provider_id: number; - /** @example Sky */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - buy?: { - /** @example /tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - CI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CI */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - CL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CL */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - CO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CO */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - CR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CR */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - CZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CZ */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 22 - */ - display_priority: number; - }[]; - }; - DE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DE */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /MiVcYLkztM6qqLeVSYWHFCUcXx.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 30 - */ - provider_id: number; - /** @example WOW */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - DK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DK */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - DO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DO */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - DZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DZ */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - EC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EC */ - link?: string; - flatrate?: { - /** @example /cDzkhgvozSr4GW2aRdV22uDuFpw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 339 - */ - provider_id: number; - /** @example Movistar Play */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - EG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EG */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - ES?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ES */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - FI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FI */ - link?: string; - buy?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - FR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FR */ - link?: string; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /loOaayvNiLnD0zKl70TO2L5vlAL.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1870 - */ - provider_id: number; - /** @example Pass Warner Amazon Channel */ - provider_name?: string; - /** - * @default 0 - * @example 95 - */ - display_priority: number; - }[]; - }; - GB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GB */ - link?: string; - flatrate?: { - /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 29 - */ - provider_id: number; - /** @example Sky Go */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - GF?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GF */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - GH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GH */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - GQ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GQ */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - GT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GT */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - HK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HK */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 40 - */ - display_priority: number; - }[]; - }; - HN?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HN */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - HR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HR */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 34 - */ - display_priority: number; - }[]; - }; - HU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HU */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 22 - */ - display_priority: number; - }[]; - }; - ID?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ID */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 14 - */ - display_priority: number; - }[]; - }; - IE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IE */ - link?: string; - flatrate?: { - /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 29 - */ - provider_id: number; - /** @example Sky Go */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - buy?: { - /** @example /2pCbao1J9s0DMak2KKnEzmzHni8.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 130 - */ - provider_id: number; - /** @example Sky Store */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - IL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IL */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - }; - IQ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IQ */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - IT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IT */ - link?: string; - buy?: { - /** @example /cksgBjTHV3rzAVaO2zUyS1mH4Ke.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 40 - */ - provider_id: number; - /** @example Chili */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /fBHHXKC34ffxAsQvDe0ZJbvmTEQ.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 29 - */ - provider_id: number; - /** @example Sky Go */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - JM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JM */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - JP?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JP */ - link?: string; - flatrate?: { - /** @example /npg1OiBidQSndMsBZwgEPOYU6Jq.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 84 - */ - provider_id: number; - /** @example U-NEXT */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - buy?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - rent?: { - /** @example /5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - KE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KE */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - KR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KR */ - link?: string; - flatrate?: { - /** @example /2ioan5BX5L9tz4fIGU93blTeFhv.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 356 - */ - provider_id: number; - /** @example wavve */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - LB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LB */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - }; - LT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LT */ - link?: string; - flatrate?: { - /** @example /xTVM8uXT9QocigQ07LE7Irc65W2.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 553 - */ - provider_id: number; - /** @example Telia Play */ - provider_name?: string; - /** - * @default 0 - * @example 15 - */ - display_priority: number; - }[]; - }; - LY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LY */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - MD?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MD */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 26 - */ - display_priority: number; - }[]; - }; - MK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MK */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - }; - MU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MU */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - MX?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MX */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - MY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MY */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 14 - */ - display_priority: number; - }[]; - }; - MZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MZ */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - NE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NE */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - NG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NG */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - NL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NL */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 47 - */ - display_priority: number; - }[]; - buy?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - NO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NO */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - buy?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - NZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NZ */ - link?: string; - flatrate?: { - /** @example /od4YNSSLgOP3p8EtQTnEYfrPa77.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 273 - */ - provider_id: number; - /** @example Neon TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - PA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PA */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - PE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PE */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - PH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PH */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - PL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PL */ - link?: string; - flatrate?: { - /** @example /l5Wxbsgral716BOtZsGyPVNn8GC.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 250 - */ - provider_id: number; - /** @example Horizon */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - rent?: { - /** @example /bZNXgd8fwVTD68aAGlElkpAtu7b.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 549 - */ - provider_id: number; - /** @example IPLA */ - provider_name?: string; - /** - * @default 0 - * @example 17 - */ - display_priority: number; - }[]; - }; - PS?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PS */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - PT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PT */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - PY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PY */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - RO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RO */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 17 - */ - display_priority: number; - }[]; - }; - RS?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RS */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 32 - */ - display_priority: number; - }[]; - }; - RU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RU */ - link?: string; - flatrate?: { - /** @example /w1T8s7FqakcfucR8cgOvbe6UeXN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 115 - */ - provider_id: number; - /** @example Okko */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - ads?: { - /** @example /3jJtMOIwtvcrCyeRMUvv4wsfhJk.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 577 - */ - provider_id: number; - /** @example TvIgle */ - provider_name?: string; - /** - * @default 0 - * @example 22 - */ - display_priority: number; - }[]; - }; - SA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SA */ - link?: string; - flatrate?: { - /** @example /xEPXbwbfABzPrUTWbgtDFH1NOa.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - SC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SC */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - SE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SE */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - buy?: { - /** @example /shq88b09gTBYC4hA7K7MUL8Q4zP.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 68 - */ - provider_id: number; - /** @example Microsoft Store */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - SG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SG */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - }; - SI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SI */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - }; - SK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SK */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 37 - */ - display_priority: number; - }[]; - }; - SN?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SN */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - SV?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SV */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - TH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TH */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - TR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TR */ - link?: string; - flatrate?: { - /** @example /z3XAGCCbDD3KTZFvc96Ytr3XR56.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 341 - */ - provider_id: number; - /** @example blutv */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - }; - TT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TT */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - TW?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TW */ - link?: string; - flatrate?: { - /** @example /bxdNcDbk1ohVeOMmM3eusAAiTLw.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 425 - */ - provider_id: number; - /** @example HBO Go */ - provider_name?: string; - /** - * @default 0 - * @example 40 - */ - display_priority: number; - }[]; - }; - TZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TZ */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - UG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UG */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - US?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=US */ - link?: string; - free?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - buy?: { - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - UY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UY */ - link?: string; - flatrate?: { - /** @example /kV8XFGI5OLJKl72dI8DtnKplfFr.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 467 - */ - provider_id: number; - /** @example DIRECTV GO */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - VE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=VE */ - link?: string; - flatrate?: { - /** @example /Ajqyt5aNxNGjmF9uOfxArGrdf3X.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 384 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - ZA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZA */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - ZM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZM */ - link?: string; - flatrate?: { - /** @example /okiQZMXnqwv0aD3QDYmu5DBNLce.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - }; - }; - }; - }; - }; - }; - 'tv-series-add-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header: { - 'Content-Type': string; - }; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - }; - }; - 'tv-series-delete-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header?: { - 'Content-Type'?: string; - }; - path: { - series_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 13 - */ - status_code: number; - /** @example The item/record was deleted successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'tv-season-details': { - parameters: { - query?: { - /** @description comma separated list of endpoints within this namespace, 20 items max */ - append_to_response?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example 5256c89f19c2956ff6046d47 */ - _id?: string; - /** @example 2011-04-17 */ - air_date?: string; - episodes?: { - /** @example 2011-04-17 */ - air_date?: string; - /** - * @default 0 - * @example 1 - */ - episode_number: number; - /** @example standard */ - episode_type?: string; - /** - * @default 0 - * @example 63056 - */ - id: number; - /** @example Winter Is Coming */ - name?: string; - /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ - overview?: string; - /** @example 101 */ - production_code?: string; - /** - * @default 0 - * @example 62 - */ - runtime: number; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** - * @default 0 - * @example 1399 - */ - show_id: number; - /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ - still_path?: string; - /** - * @default 0 - * @example 8.1 - */ - vote_average: number; - /** - * @default 0 - * @example 396 - */ - vote_count: number; - crew?: { - /** @example Directing */ - department?: string; - /** @example Director */ - job?: string; - /** @example 5256c8a219c2956ff6046e77 */ - credit_id?: string; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 44797 - */ - id: number; - /** @example Directing */ - known_for_department?: string; - /** @example Tim Van Patten */ - name?: string; - /** @example Tim Van Patten */ - original_name?: string; - /** - * @default 0 - * @example 0.8004 - */ - popularity: number; - /** @example /vwcARZBg4PEzOwnPsXdjRWeUVrZ.jpg */ - profile_path?: string; - }[]; - guest_stars?: { - /** @example Benjen Stark */ - character?: string; - /** @example 5256c8b919c2956ff604836a */ - credit_id?: string; - /** - * @default 0 - * @example 61 - */ - order: number; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 119783 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Joseph Mawle */ - name?: string; - /** @example Joseph Mawle */ - original_name?: string; - /** - * @default 0 - * @example 0.8932 - */ - popularity: number; - /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ - profile_path?: string; - }[]; - }[]; - /** @example Season 1 */ - name?: string; - networks?: { - /** - * @default 0 - * @example 49 - */ - id: number; - /** @example /tuomPhY2UtuPTqqFnKMVHvSb724.png */ - logo_path?: string; - /** @example HBO */ - name?: string; - /** @example US */ - origin_country?: string; - }[]; - /** @example Trouble is brewing in the Seven Kingdoms of Westeros. For the driven inhabitants of this visionary world, control of Westeros' Iron Throne holds the lure of great power. But in a land where the seasons can last a lifetime, winter is coming...and beyond the Great Wall that protects them, an ancient evil has returned. In Season One, the story centers on three primary areas: the Stark and the Lannister families, whose designs on controlling the throne threaten a tenuous peace; the dragon princess Daenerys, heir to the former dynasty, who waits just over the Narrow Sea with her malevolent brother Viserys; and the Great Wall--a massive barrier of ice where a forgotten danger is stirring. */ - overview?: string; - /** - * @default 0 - * @example 3624 - */ - id: number; - /** @example /wgfKiqzuMrFIkU1M68DDDY8kGC1.jpg */ - poster_path?: string; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** - * @default 0 - * @example 8.4 - */ - vote_average: number; - }; - }; - }; - }; - }; - 'tv-season-account-states': { - parameters: { - query?: { - session_id?: string; - guest_session_id?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - results?: { - /** - * @default 0 - * @example 63056 - */ - id: number; - /** - * @default 0 - * @example 1 - */ - episode_number: number; - rated?: { - /** - * @default 0 - * @example 9 - */ - value: number; - }; - }[]; - }; - }; - }; - }; - }; - 'tv-season-aggregate-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 22970 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Peter Dinklage */ - name?: string; - /** @example Peter Dinklage */ - original_name?: string; - /** - * @default 0 - * @example 30.6 - */ - popularity: number; - /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ - profile_path?: string; - roles?: { - /** @example 5256c8b219c2956ff6047cd8 */ - credit_id?: string; - /** @example Tyrion Lannister */ - character?: string; - /** - * @default 0 - * @example 10 - */ - episode_count: number; - }[]; - /** - * @default 0 - * @example 10 - */ - total_episode_count: number; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 1 - */ - gender: number; - /** - * @default 0 - * @example 9153 - */ - id: number; - /** @example Art */ - known_for_department?: string; - /** @example Gemma Jackson */ - name?: string; - /** @example Gemma Jackson */ - original_name?: string; - /** - * @default 0 - * @example 0.995 - */ - popularity: number; - profile_path?: unknown; - jobs?: { - /** @example 54eee8b8c3a3686d5e005430 */ - credit_id?: string; - /** @example Production Design */ - job?: string; - /** - * @default 0 - * @example 10 - */ - episode_count: number; - }[]; - /** @example Art */ - department?: string; - /** - * @default 0 - * @example 10 - */ - total_episode_count: number; - }[]; - /** - * @default 0 - * @example 3624 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-season-changes-by-id': { - parameters: { - query?: { - end_date?: string; - page?: number; - start_date?: string; - }; - header?: never; - path: { - season_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - changes?: { - /** @example episode */ - key?: string; - items?: { - /** @example 5717c8c69251414cfd00250f */ - id?: string; - /** @example updated */ - action?: string; - /** @example 2016-04-20 18:21:58 UTC */ - time?: string; - value?: { - /** - * @default 0 - * @example 63056 - */ - episode_id: number; - /** - * @default 0 - * @example 1 - */ - episode_number: number; - }; - }[]; - }[]; - }; - }; - }; - }; - }; - 'tv-season-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 22970 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Peter Dinklage */ - name?: string; - /** @example Peter Dinklage */ - original_name?: string; - /** - * @default 0 - * @example 30.6 - */ - popularity: number; - /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ - profile_path?: string; - /** @example Tyrion Lannister */ - character?: string; - /** @example 5256c8b219c2956ff6047cd8 */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 0 - */ - gender: number; - /** - * @default 0 - * @example 1223796 - */ - id: number; - /** @example Production */ - known_for_department?: string; - /** @example Frank Doelger */ - name?: string; - /** @example Frank Doelger */ - original_name?: string; - /** - * @default 0 - * @example 0.694 - */ - popularity: number; - profile_path?: unknown; - /** @example 5256c8c419c2956ff604867c */ - credit_id?: string; - /** @example Production */ - department?: string; - /** @example Producer */ - job?: string; - }[]; - /** - * @default 0 - * @example 3624 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-season-external-ids': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - /** @example /m/0gmd1gd */ - freebase_mid?: string; - /** @example /m/0gmd1gd */ - freebase_id?: string; - /** - * @default 0 - * @example 364731 - */ - tvdb_id: number; - tvrage_id?: unknown; - /** @example Q1658029 */ - wikidata_id?: string; - }; - }; - }; - }; - }; - 'tv-season-images': { - parameters: { - query?: { - /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ - include_image_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - posters?: { - /** - * @default 0 - * @example 0.667 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 1500 - */ - height: number; - /** @example en */ - iso_639_1?: string; - /** @example /wgfKiqzuMrFIkU1M68DDDY8kGC1.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.514 - */ - vote_average: number; - /** - * @default 0 - * @example 18 - */ - vote_count: number; - /** - * @default 0 - * @example 1000 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'tv-season-translations': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - translations?: { - /** @example SA */ - iso_3166_1?: string; - /** @example ar */ - iso_639_1?: string; - /** @example العربية */ - name?: string; - /** @example Arabic */ - english_name?: string; - data?: { - /** @example */ - name?: string; - /** @example سلسلة درامية مبنية على سلسلة روايات لـ جورج آر آر مارتن بعنوان "إيه سونغ أوف آيس أن فاير" والتي حققت مبيعات كبيرة وتتمحور حول الصراعات التي كانت تحدث في العصور الوسطى بين العائلات النبيلة للسيطرة على عرش وستيروس. */ - overview?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'tv-season-videos': { - parameters: { - query?: { - /** @description filter the list results by language, supports more than one value by using a comma */ - include_video_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - results?: { - /** @example en */ - iso_639_1?: string; - /** @example US */ - iso_3166_1?: string; - /** @example Game Of Thrones - Season 1 Recap - Official HBO UK */ - name?: string; - /** @example e0Y8KpQpW8c */ - key?: string; - /** @example YouTube */ - site?: string; - /** - * @default 0 - * @example 1080 - */ - size: number; - /** @example Recap */ - type?: string; - /** - * @default true - * @example true - */ - official: boolean; - /** @example 2015-05-19T16:31:23.000Z */ - published_at?: string; - /** @example 5ce71a920e0a265ac0cfe497 */ - id?: string; - }[]; - }; - }; - }; - }; - }; - 'tv-season-watch-providers': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - results?: { - AD?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AD */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 24 - */ - display_priority: number; - }[]; - }; - AE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AE */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - AG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AG */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - AR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AR */ - link?: string; - flatrate?: { - /** @example /nr5UBW4IGKgBwmhpTMOfcvnX2vX.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 467 - */ - provider_id: number; - /** @example DIRECTV GO */ - provider_name?: string; - /** - * @default 0 - * @example 12 - */ - display_priority: number; - }[]; - }; - AT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AT */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /kAZkQcIxMxTmlwdgSB05fqtymp0.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 29 - */ - provider_id: number; - /** @example Sky Go */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - }; - AU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=AU */ - link?: string; - flatrate?: { - /** @example /fejdSG7TwNQ5E0p6u7A6LVs280R.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 134 - */ - provider_id: number; - /** @example Foxtel Now */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - buy?: { - /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - BA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BA */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - BB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BB */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 23 - */ - display_priority: number; - }[]; - }; - BE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BE */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 33 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /kwftIxtjuCAROIcdd53UEjzSmca.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1857 - */ - provider_id: number; - /** @example Telenet */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - BG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BG */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 21 - */ - display_priority: number; - }[]; - }; - BH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BH */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - }; - BO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BO */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - BR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BR */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - BS?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BS */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 24 - */ - display_priority: number; - }[]; - }; - BZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=BZ */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - CA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CA */ - link?: string; - flatrate?: { - /** @example /ewOptMVIYcOadMGGJz8DJueH2bH.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 230 - */ - provider_id: number; - /** @example Crave */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - buy?: { - /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - CH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CH */ - link?: string; - buy?: { - /** @example /8z7rC8uIDaTM91X0ZfkRf04ydj2.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 3 - */ - provider_id: number; - /** @example Google Play Movies */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /ytApMa9fThUQUFTn696AeNBrB8f.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 210 - */ - provider_id: number; - /** @example Sky */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - CI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CI */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 19 - */ - display_priority: number; - }[]; - }; - CL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CL */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - CM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CM */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - CO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CO */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - CR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CR */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - CZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=CZ */ - link?: string; - flatrate?: { - /** @example /489t5n9o1KhH7voGNQkrXT7vBKV.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1939 - */ - provider_id: number; - /** @example Lepsi TV */ - provider_name?: string; - /** - * @default 0 - * @example 26 - */ - display_priority: number; - }[]; - }; - DE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DE */ - link?: string; - flatrate?: { - /** @example /kAZkQcIxMxTmlwdgSB05fqtymp0.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 29 - */ - provider_id: number; - /** @example Sky Go */ - provider_name?: string; - /** - * @default 0 - * @example 8 - */ - display_priority: number; - }[]; - buy?: { - /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - DK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DK */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 39 - */ - display_priority: number; - }[]; - }; - DO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=DO */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - EC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EC */ - link?: string; - flatrate?: { - /** @example /tRNA2CRgA4XHvd7Mx9dH3sFtDVb.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 339 - */ - provider_id: number; - /** @example MovistarTV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - EG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=EG */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 20 - */ - display_priority: number; - }[]; - }; - ES?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ES */ - link?: string; - flatrate?: { - /** @example /f6TRLB3H4jDpFEZ0z2KWSSvu1SB.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 149 - */ - provider_id: number; - /** @example Movistar Plus+ Ficción Total */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 32 - */ - display_priority: number; - }[]; - }; - FI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FI */ - link?: string; - flatrate?: { - /** @example /eglAxQEXSO13p6gNf3HKymrIu7y.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 540 - */ - provider_id: number; - /** @example Elisa Viihde */ - provider_name?: string; - /** - * @default 0 - * @example 18 - */ - display_priority: number; - }[]; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 54 - */ - display_priority: number; - }[]; - }; - FR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=FR */ - link?: string; - buy?: { - /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 78 - */ - display_priority: number; - }[]; - }; - GB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GB */ - link?: string; - buy?: { - /** @example /oMYZg3cGAGp9ecKGlBgumcjDmnN.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 39 - */ - provider_id: number; - /** @example Now TV */ - provider_name?: string; - /** - * @default 0 - * @example 43 - */ - display_priority: number; - }[]; - }; - GG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GG */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 84 - */ - display_priority: number; - }[]; - }; - GQ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GQ */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - GT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GT */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - GY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=GY */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - HK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HK */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 38 - */ - display_priority: number; - }[]; - }; - HN?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HN */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - HR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HR */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 33 - */ - display_priority: number; - }[]; - }; - HU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=HU */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - ID?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ID */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 39 - */ - display_priority: number; - }[]; - }; - IE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IE */ - link?: string; - flatrate?: { - /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 39 - */ - provider_id: number; - /** @example Now TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - buy?: { - /** @example /6AKbY2ayaEuH4zKg2prqoVQ9iaY.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 130 - */ - provider_id: number; - /** @example Sky Store */ - provider_name?: string; - /** - * @default 0 - * @example 9 - */ - display_priority: number; - }[]; - }; - IN?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IN */ - link?: string; - flatrate?: { - /** @example /kVqjgpcwvDJOhCupjcLzwwtOp52.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 2336 - */ - provider_id: number; - /** @example JioHotstar */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - IQ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IQ */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - IT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=IT */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 33 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /g0E9h3JAeIwmdvxlT73jiEuxdNj.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 39 - */ - provider_id: number; - /** @example Now TV */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - JM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JM */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 22 - */ - display_priority: number; - }[]; - }; - JO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JO */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - JP?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=JP */ - link?: string; - flatrate?: { - /** @example /a5T7vNaGvoeckYO6rQkHolvyYf4.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 84 - */ - provider_id: number; - /** @example U-NEXT */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - }[]; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - rent?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - KE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=KE */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - LB?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LB */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - LC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=LC */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - MC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MC */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - MD?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MD */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 25 - */ - display_priority: number; - }[]; - }; - ME?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ME */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - MG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MG */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - MK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MK */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 26 - */ - display_priority: number; - }[]; - }; - ML?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ML */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - MU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MU */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - MX?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MX */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 5 - */ - display_priority: number; - }[]; - }; - MY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MY */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - MZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=MZ */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - NE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NE */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 19 - */ - display_priority: number; - }[]; - }; - NG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NG */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 21 - */ - display_priority: number; - }[]; - }; - NI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NI */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 13 - */ - display_priority: number; - }[]; - }; - NL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NL */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 34 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 48 - */ - display_priority: number; - }[]; - }; - NO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NO */ - link?: string; - flatrate?: { - /** @example /3ZigBD8WTEPcEHAvMWiJGUsv5u4.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 578 - */ - provider_id: number; - /** @example Strim */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - NZ?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=NZ */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 59 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /iscLKFDwQlr0BAgVDBcuRapLiwC.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 273 - */ - provider_id: number; - /** @example Neon TV */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - OM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=OM */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - PA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PA */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - PE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PE */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - PH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PH */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 32 - */ - display_priority: number; - }[]; - }; - PL?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PL */ - link?: string; - flatrate?: { - /** @example /jhMNVBV2UocEGepRkr9oFPD7Gpb.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 505 - */ - provider_id: number; - /** @example Player */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - PT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PT */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 43 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 32 - */ - display_priority: number; - }[]; - }; - PY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=PY */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - }; - QA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=QA */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - RO?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RO */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 23 - */ - display_priority: number; - }[]; - }; - RS?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RS */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - RU?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=RU */ - link?: string; - flatrate?: { - /** @example /5z8dpQN27kybhn21EVLZcJPpMEo.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 115 - */ - provider_id: number; - /** @example Okko */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - SA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SA */ - link?: string; - flatrate?: { - /** @example /kC6JTo59Gj6I4vJPyBAYGh0sKAE.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 629 - */ - provider_id: number; - /** @example OSN+ */ - provider_name?: string; - /** - * @default 0 - * @example 20 - */ - display_priority: number; - }[]; - }; - SC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SC */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - SE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SE */ - link?: string; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 39 - */ - display_priority: number; - }[]; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 44 - */ - display_priority: number; - }[]; - }; - SG?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SG */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 31 - */ - display_priority: number; - }[]; - }; - SI?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SI */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 28 - */ - display_priority: number; - }[]; - }; - SK?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SK */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 37 - */ - display_priority: number; - }[]; - }; - SN?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SN */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - SV?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=SV */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - TC?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TC */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 10 - */ - display_priority: number; - }[]; - }; - TD?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TD */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - TH?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TH */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 30 - */ - display_priority: number; - }[]; - }; - TR?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TR */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 29 - */ - display_priority: number; - }[]; - }; - TT?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TT */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - }; - TW?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=TW */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 38 - */ - display_priority: number; - }[]; - }; - US?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=US */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 11 - */ - display_priority: number; - }[]; - buy?: { - /** @example /seGSXajazLMCKGB5hnRCidtjay1.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 10 - */ - provider_id: number; - /** @example Amazon Video */ - provider_name?: string; - /** - * @default 0 - * @example 6 - */ - display_priority: number; - }[]; - }; - UY?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=UY */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 3 - */ - display_priority: number; - }[]; - }; - VE?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=VE */ - link?: string; - flatrate?: { - /** @example /jbe4gVSfRlbPTdESXhEKpornsfu.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 1899 - */ - provider_id: number; - /** @example HBO Max */ - provider_name?: string; - /** - * @default 0 - * @example 27 - */ - display_priority: number; - }[]; - }; - ZA?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZA */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 4 - */ - display_priority: number; - }[]; - }; - ZM?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZM */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 7 - */ - display_priority: number; - }[]; - }; - ZW?: { - /** @example https://www.themoviedb.org/tv/1399-game-of-thrones/watch?locale=ZW */ - link?: string; - flatrate?: { - /** @example /ilR8XFZOr3e3wETQ4ZXLjXQXrts.jpg */ - logo_path?: string; - /** - * @default 0 - * @example 55 - */ - provider_id: number; - /** @example ShowMax */ - provider_name?: string; - /** - * @default 0 - * @example 0 - */ - display_priority: number; - }[]; - }; - }; - }; - }; - }; - }; - }; - 'tv-episode-details': { - parameters: { - query?: { - /** @description comma separated list of endpoints within this namespace, 20 items max */ - append_to_response?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example 2011-04-17 */ - air_date?: string; - crew?: { - /** @example Directing */ - department?: string; - /** @example Director */ - job?: string; - /** @example 5256c8a219c2956ff6046e77 */ - credit_id?: string; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 44797 - */ - id: number; - /** @example Directing */ - known_for_department?: string; - /** @example Timothy Van Patten */ - name?: string; - /** @example Timothy Van Patten */ - original_name?: string; - /** - * @default 0 - * @example 7.775 - */ - popularity: number; - /** @example /MzSOFrd99HRdr6pkSRSctk3kBR.jpg */ - profile_path?: string; - }[]; - /** - * @default 0 - * @example 1 - */ - episode_number: number; - guest_stars?: { - /** @example Benjen Stark */ - character?: string; - /** @example 5256c8b919c2956ff604836a */ - credit_id?: string; - /** - * @default 0 - * @example 62 - */ - order: number; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 119783 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Joseph Mawle */ - name?: string; - /** @example Joseph Mawle */ - original_name?: string; - /** - * @default 0 - * @example 6.758 - */ - popularity: number; - /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ - profile_path?: string; - }[]; - /** @example Winter Is Coming */ - name?: string; - /** @example Jon Arryn, the Hand of the King, is dead. King Robert Baratheon plans to ask his oldest friend, Eddard Stark, to take Jon's place. Across the sea, Viserys Targaryen plans to wed his sister to a nomadic warlord in exchange for an army. */ - overview?: string; - /** - * @default 0 - * @example 63056 - */ - id: number; - /** @example 101 */ - production_code?: string; - /** - * @default 0 - * @example 62 - */ - runtime: number; - /** - * @default 0 - * @example 1 - */ - season_number: number; - /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ - still_path?: string; - /** - * @default 0 - * @example 7.8 - */ - vote_average: number; - /** - * @default 0 - * @example 286 - */ - vote_count: number; - }; - }; - }; - }; - }; - 'tv-episode-account-states': { - parameters: { - query?: { - session_id?: string; - guest_session_id?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 550 - */ - id: number; - /** - * @default true - * @example true - */ - favorite: boolean; - rated?: { - /** - * @default 0 - * @example 9 - */ - value: number; - }; - /** - * @default true - * @example false - */ - watchlist: boolean; - }; - }; - }; - }; - }; - 'tv-episode-changes-by-id': { - parameters: { - query?: never; - header?: never; - path: { - episode_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - changes?: { - /** @example production_code */ - key?: string; - items?: { - /** @example 54bd9ed7c3a3686c6b00da66 */ - id?: string; - /** @example added */ - action?: string; - /** @example 2015-01-20 00:18:31 UTC */ - time?: string; - /** @example 101 */ - value?: string; - }[]; - }[]; - }; - }; - }; - }; - }; - 'tv-episode-credits': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - cast?: { - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 22970 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Peter Dinklage */ - name?: string; - /** @example Peter Dinklage */ - original_name?: string; - /** - * @default 0 - * @example 30.6 - */ - popularity: number; - /** @example /lRsRgnksAhBRXwAB68MFjmTtLrk.jpg */ - profile_path?: string; - /** @example Tyrion Lannister */ - character?: string; - /** @example 5256c8b219c2956ff6047cd8 */ - credit_id?: string; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - crew?: { - /** @example Directing */ - department?: string; - /** @example Director */ - job?: string; - /** @example 5256c8a219c2956ff6046e77 */ - credit_id?: string; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 44797 - */ - id: number; - /** @example Directing */ - known_for_department?: string; - /** @example Timothy Van Patten */ - name?: string; - /** @example Timothy Van Patten */ - original_name?: string; - /** - * @default 0 - * @example 8.292 - */ - popularity: number; - /** @example /MzSOFrd99HRdr6pkSRSctk3kBR.jpg */ - profile_path?: string; - }[]; - guest_stars?: { - /** @example Benjen Stark */ - character?: string; - /** @example 5256c8b919c2956ff604836a */ - credit_id?: string; - /** - * @default 0 - * @example 62 - */ - order: number; - /** - * @default true - * @example false - */ - adult: boolean; - /** - * @default 0 - * @example 2 - */ - gender: number; - /** - * @default 0 - * @example 119783 - */ - id: number; - /** @example Acting */ - known_for_department?: string; - /** @example Joseph Mawle */ - name?: string; - /** @example Joseph Mawle */ - original_name?: string; - /** - * @default 0 - * @example 8.559 - */ - popularity: number; - /** @example /1Ocb9v3h54beGVoJMm4w50UQhLf.jpg */ - profile_path?: string; - }[]; - /** - * @default 0 - * @example 63056 - */ - id: number; - }; - }; - }; - }; - }; - 'tv-episode-external-ids': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 63056 - */ - id: number; - /** @example tt1480055 */ - imdb_id?: string; - /** @example /m/0gmc6ph */ - freebase_mid?: string; - /** @example /en/winter_is_coming */ - freebase_id?: string; - /** - * @default 0 - * @example 3254641 - */ - tvdb_id: number; - /** - * @default 0 - * @example 1065008299 - */ - tvrage_id: number; - /** @example Q2614622 */ - wikidata_id?: string; - }; - }; - }; - }; - }; - 'tv-episode-images': { - parameters: { - query?: { - /** @description specify a comma separated list of ISO-639-1 values to query, for example: `en-US,null` */ - include_image_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 63056 - */ - id: number; - stills?: { - /** - * @default 0 - * @example 1.778 - */ - aspect_ratio: number; - /** - * @default 0 - * @example 1080 - */ - height: number; - iso_639_1?: unknown; - /** @example /9hGF3WUkBf7cSjMg0cdMDHJkByd.jpg */ - file_path?: string; - /** - * @default 0 - * @example 5.454 - */ - vote_average: number; - /** - * @default 0 - * @example 3 - */ - vote_count: number; - /** - * @default 0 - * @example 1920 - */ - width: number; - }[]; - }; - }; - }; - }; - }; - 'tv-episode-translations': { - parameters: { - query?: never; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 63056 - */ - id: number; - translations?: { - /** @example SA */ - iso_3166_1?: string; - /** @example ar */ - iso_639_1?: string; - /** @example العربية */ - name?: string; - /** @example Arabic */ - english_name?: string; - data?: { - /** @example */ - name?: string; - /** @example خلف باب واسع من الجليد في شمالي وستيروس هناك شيء يحدث. تتلقى عائلة ستارك التي من وينترفيل زيارة من العائلة المالكة، بينما يشكل أمير عائلة تارغارين المنفي تحالفاً جديداً للسيطرة على العرش من جديد. */ - overview?: string; - }; - }[]; - }; - }; - }; - }; - }; - 'tv-episode-videos': { - parameters: { - query?: { - /** @description filter the list results by language, supports more than one value by using a comma */ - include_video_language?: string; - language?: string; - }; - header?: never; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 3624 - */ - id: number; - results?: { - /** @example en */ - iso_639_1?: string; - /** @example US */ - iso_3166_1?: string; - /** @example Game Of Thrones - Season 1 Recap - Official HBO UK */ - name?: string; - /** @example e0Y8KpQpW8c */ - key?: string; - /** @example YouTube */ - site?: string; - /** - * @default 0 - * @example 1080 - */ - size: number; - /** @example Recap */ - type?: string; - /** - * @default true - * @example true - */ - official: boolean; - /** @example 2015-05-19T16:31:23.000Z */ - published_at?: string; - /** @example 5ce71a920e0a265ac0cfe497 */ - id?: string; - }[]; - }; - }; - }; - }; - }; - 'tv-episode-add-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header: { - 'Content-Type': string; - }; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: { - content: { - 'application/json': { - /** Format: json */ - RAW_BODY: string; - }; - }; - }; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 1 - */ - status_code: number; - /** @example Success. */ - status_message?: string; - }; - }; - }; - }; - }; - 'tv-episode-delete-rating': { - parameters: { - query?: { - guest_session_id?: string; - session_id?: string; - }; - header?: { - 'Content-Type'?: string; - }; - path: { - series_id: number; - season_number: number; - episode_number: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** - * @default 0 - * @example 13 - */ - status_code: number; - /** @example The item/record was deleted successfully. */ - status_message?: string; - }; - }; - }; - }; - }; - 'tv-episode-group-details': { - parameters: { - query?: never; - header?: never; - path: { - tv_episode_group_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example Comedians in Cars organized in Netflix's collections. */ - description?: string; - /** - * @default 0 - * @example 83 - */ - episode_count: number; - /** - * @default 0 - * @example 6 - */ - group_count: number; - groups?: { - /** @example 5acf93efc3a368739a0000a9 */ - id?: string; - /** @example First Cup */ - name?: string; - /** - * @default 0 - * @example 1 - */ - order: number; - episodes?: { - /** @example 2015-06-17 */ - air_date?: string; - /** - * @default 0 - * @example 3 - */ - episode_number: number; - /** - * @default 0 - * @example 1078262 - */ - id: number; - /** @example Jim Carrey: We Love Breathing What You're Burning, Baby */ - name?: string; - /** @example Jerry’s full of testosterone as he steps into a ‘76 Lamborghini Countach with Jim Carrey, who’s between a three-week cleanse and a five-day silent retreat. After coffee, it’s off to Carrey’s studio to study a portrait of a gorilla with a machine gun. Wow. */ - overview?: string; - /** @example */ - production_code?: string; - runtime?: unknown; - /** - * @default 0 - * @example 6 - */ - season_number: number; - /** - * @default 0 - * @example 59717 - */ - show_id: number; - /** @example /aOyE420zuFq9zWtEWjIccAiTrzU.jpg */ - still_path?: string; - /** - * @default 0 - * @example 7.4 - */ - vote_average: number; - /** - * @default 0 - * @example 5 - */ - vote_count: number; - /** - * @default 0 - * @example 0 - */ - order: number; - }[]; - /** - * @default true - * @example true - */ - locked: boolean; - }[]; - /** @example 5acf93e60e0a26346d0000ce */ - id?: string; - /** @example Netflix Collections */ - name?: string; - network?: { - /** - * @default 0 - * @example 213 - */ - id: number; - /** @example /wwemzKWzjKYJFfCeiB57q3r4Bcm.png */ - logo_path?: string; - /** @example Netflix */ - name?: string; - /** @example */ - origin_country?: string; - }; - /** - * @default 0 - * @example 4 - */ - type: number; - }; - }; - }; - }; - }; - 'watch-providers-available-regions': { - parameters: { - query?: { - language?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - /** @example AD */ - iso_3166_1?: string; - /** @example Andorra */ - english_name?: string; - /** @example Andorra */ - native_name?: string; - }[]; - }; - }; - }; - }; - }; - 'watch-providers-movie-list': { - parameters: { - query?: { - language?: string; - watch_region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - display_priorities?: { - /** - * @default 0 - * @example 6 - */ - CA: number; - /** - * @default 0 - * @example 1 - */ - AE: number; - /** - * @default 0 - * @example 3 - */ - AR: number; - /** - * @default 0 - * @example 4 - */ - AT: number; - /** - * @default 0 - * @example 10 - */ - AU: number; - /** - * @default 0 - * @example 6 - */ - BE: number; - /** - * @default 0 - * @example 6 - */ - BO: number; - /** - * @default 0 - * @example 8 - */ - BR: number; - /** - * @default 0 - * @example 2 - */ - BG: number; - /** - * @default 0 - * @example 4 - */ - CH: number; - /** - * @default 0 - * @example 3 - */ - CL: number; - /** - * @default 0 - * @example 4 - */ - CO: number; - /** - * @default 0 - * @example 5 - */ - CR: number; - /** - * @default 0 - * @example 3 - */ - CZ: number; - /** - * @default 0 - * @example 4 - */ - DE: number; - /** - * @default 0 - * @example 7 - */ - DK: number; - /** - * @default 0 - * @example 7 - */ - EC: number; - /** - * @default 0 - * @example 3 - */ - EE: number; - /** - * @default 0 - * @example 2 - */ - EG: number; - /** - * @default 0 - * @example 4 - */ - ES: number; - /** - * @default 0 - * @example 10 - */ - FI: number; - /** - * @default 0 - * @example 5 - */ - FR: number; - /** - * @default 0 - * @example 5 - */ - GB: number; - /** - * @default 0 - * @example 2 - */ - GR: number; - /** - * @default 0 - * @example 7 - */ - GT: number; - /** - * @default 0 - * @example 5 - */ - HK: number; - /** - * @default 0 - * @example 7 - */ - HN: number; - /** - * @default 0 - * @example 3 - */ - HU: number; - /** - * @default 0 - * @example 4 - */ - ID: number; - /** - * @default 0 - * @example 4 - */ - IE: number; - /** - * @default 0 - * @example 8 - */ - IN: number; - /** - * @default 0 - * @example 4 - */ - IT: number; - /** - * @default 0 - * @example 7 - */ - JP: number; - /** - * @default 0 - * @example 3 - */ - LT: number; - /** - * @default 0 - * @example 3 - */ - LV: number; - /** - * @default 0 - * @example 4 - */ - MX: number; - /** - * @default 0 - * @example 4 - */ - MY: number; - /** - * @default 0 - * @example 8 - */ - NL: number; - /** - * @default 0 - * @example 6 - */ - NO: number; - /** - * @default 0 - * @example 4 - */ - NZ: number; - /** - * @default 0 - * @example 3 - */ - PE: number; - /** - * @default 0 - * @example 4 - */ - PH: number; - /** - * @default 0 - * @example 1 - */ - PL: number; - /** - * @default 0 - * @example 4 - */ - PT: number; - /** - * @default 0 - * @example 7 - */ - PY: number; - /** - * @default 0 - * @example 2 - */ - RU: number; - /** - * @default 0 - * @example 1 - */ - SA: number; - /** - * @default 0 - * @example 8 - */ - SE: number; - /** - * @default 0 - * @example 5 - */ - SG: number; - /** - * @default 0 - * @example 3 - */ - SK: number; - /** - * @default 0 - * @example 4 - */ - TH: number; - /** - * @default 0 - * @example 6 - */ - TR: number; - /** - * @default 0 - * @example 7 - */ - TW: number; - /** - * @default 0 - * @example 4 - */ - US: number; - /** - * @default 0 - * @example 4 - */ - VE: number; - /** - * @default 0 - * @example 2 - */ - ZA: number; - /** - * @default 0 - * @example 31 - */ - SI: number; - /** - * @default 0 - * @example 13 - */ - CV: number; - /** - * @default 0 - * @example 17 - */ - GH: number; - /** - * @default 0 - * @example 15 - */ - MU: number; - /** - * @default 0 - * @example 16 - */ - MZ: number; - /** - * @default 0 - * @example 16 - */ - UG: number; - /** - * @default 0 - * @example 28 - */ - IL: number; - }; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - }[]; - }; - }; - }; - }; - }; - 'watch-provider-tv-list': { - parameters: { - query?: { - language?: string; - watch_region?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description 200 */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - results?: { - display_priorities?: { - /** - * @default 0 - * @example 6 - */ - CA: number; - /** - * @default 0 - * @example 1 - */ - AE: number; - /** - * @default 0 - * @example 3 - */ - AR: number; - /** - * @default 0 - * @example 4 - */ - AT: number; - /** - * @default 0 - * @example 10 - */ - AU: number; - /** - * @default 0 - * @example 6 - */ - BE: number; - /** - * @default 0 - * @example 6 - */ - BO: number; - /** - * @default 0 - * @example 8 - */ - BR: number; - /** - * @default 0 - * @example 2 - */ - BG: number; - /** - * @default 0 - * @example 4 - */ - CH: number; - /** - * @default 0 - * @example 3 - */ - CL: number; - /** - * @default 0 - * @example 4 - */ - CO: number; - /** - * @default 0 - * @example 5 - */ - CR: number; - /** - * @default 0 - * @example 3 - */ - CZ: number; - /** - * @default 0 - * @example 4 - */ - DE: number; - /** - * @default 0 - * @example 7 - */ - DK: number; - /** - * @default 0 - * @example 7 - */ - EC: number; - /** - * @default 0 - * @example 3 - */ - EE: number; - /** - * @default 0 - * @example 2 - */ - EG: number; - /** - * @default 0 - * @example 4 - */ - ES: number; - /** - * @default 0 - * @example 10 - */ - FI: number; - /** - * @default 0 - * @example 5 - */ - FR: number; - /** - * @default 0 - * @example 5 - */ - GB: number; - /** - * @default 0 - * @example 2 - */ - GR: number; - /** - * @default 0 - * @example 7 - */ - GT: number; - /** - * @default 0 - * @example 5 - */ - HK: number; - /** - * @default 0 - * @example 7 - */ - HN: number; - /** - * @default 0 - * @example 3 - */ - HU: number; - /** - * @default 0 - * @example 4 - */ - ID: number; - /** - * @default 0 - * @example 4 - */ - IE: number; - /** - * @default 0 - * @example 8 - */ - IN: number; - /** - * @default 0 - * @example 4 - */ - IT: number; - /** - * @default 0 - * @example 7 - */ - JP: number; - /** - * @default 0 - * @example 3 - */ - LT: number; - /** - * @default 0 - * @example 3 - */ - LV: number; - /** - * @default 0 - * @example 4 - */ - MX: number; - /** - * @default 0 - * @example 4 - */ - MY: number; - /** - * @default 0 - * @example 8 - */ - NL: number; - /** - * @default 0 - * @example 6 - */ - NO: number; - /** - * @default 0 - * @example 4 - */ - NZ: number; - /** - * @default 0 - * @example 3 - */ - PE: number; - /** - * @default 0 - * @example 4 - */ - PH: number; - /** - * @default 0 - * @example 1 - */ - PL: number; - /** - * @default 0 - * @example 4 - */ - PT: number; - /** - * @default 0 - * @example 7 - */ - PY: number; - /** - * @default 0 - * @example 2 - */ - RU: number; - /** - * @default 0 - * @example 1 - */ - SA: number; - /** - * @default 0 - * @example 8 - */ - SE: number; - /** - * @default 0 - * @example 5 - */ - SG: number; - /** - * @default 0 - * @example 3 - */ - SK: number; - /** - * @default 0 - * @example 4 - */ - TH: number; - /** - * @default 0 - * @example 6 - */ - TR: number; - /** - * @default 0 - * @example 7 - */ - TW: number; - /** - * @default 0 - * @example 4 - */ - US: number; - /** - * @default 0 - * @example 4 - */ - VE: number; - /** - * @default 0 - * @example 2 - */ - ZA: number; - /** - * @default 0 - * @example 31 - */ - SI: number; - /** - * @default 0 - * @example 13 - */ - CV: number; - /** - * @default 0 - * @example 17 - */ - GH: number; - /** - * @default 0 - * @example 15 - */ - MU: number; - /** - * @default 0 - * @example 16 - */ - MZ: number; - /** - * @default 0 - * @example 16 - */ - UG: number; - /** - * @default 0 - * @example 28 - */ - IL: number; - }; - /** - * @default 0 - * @example 2 - */ - display_priority: number; - /** @example /peURlLlr8jggOwK53fJ5wdQl05y.jpg */ - logo_path?: string; - /** @example Apple TV */ - provider_name?: string; - /** - * @default 0 - * @example 2 - */ - provider_id: number; - }[]; - }; - }; - }; - }; - }; -} From cfadb8d9b852247f372331514671af5fd2e38d7a Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:35:52 +0300 Subject: [PATCH 40/60] another fix Added markdown report generation for BulkUpdateHelper, AutoTrackerHelper, and main.ts (Download Images). Failed files are now logged directly to /MDB - error report .md instead of just logging to the console. Filename formatting standardized to match the existing Bulk Import feature layout. Modified downloadImagesInFile to return rich {success, error, skipped} objects for exact failure tracking. --- src/main.ts | 39 +++++++++++++++++++++++----------- src/utils/AutoTrackerHelper.ts | 13 +++++++++++- src/utils/BulkUpdateHelper.ts | 13 +++++++++++- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index 9e655a91..8705b47c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -48,7 +48,7 @@ import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; import type { CreateNoteOptions } from './utils/Utils'; import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; -import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile } from './utils/Utils'; +import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable } from './utils/Utils'; import 'src/styles.css'; export type Metadata = Record; @@ -797,15 +797,30 @@ export default class MediaDbPlugin extends Plugin { const startTime = Date.now(); let downloaded = 0; let failed = 0; + const erroredFiles: { filePath: string, error: string }[] = []; + for (const file of files) { const result = await this.downloadImagesInFile(file, true); - if (result === true) downloaded++; - else if (result === false) failed++; - // null means no image to download - if (result !== null) { - await new Promise(r => setTimeout(r, 600)); // anti-rate limit + if (result.success) { + downloaded++; + } else if (!result.skipped) { + failed++; + if (result.error) erroredFiles.push({ filePath: file.path, error: result.error }); + } + // wait slightly as anti-rate limit + if (!result.skipped) { + await new Promise(r => setTimeout(r, 600)); } } + + if (failed > 0 && erroredFiles.length > 0) { + const title = `MDB - image download error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.app.vault.create(filePath, fileContent); + } + new CompletionModal(this.app, { title: 'Image Download Complete', icon: '🖼️', @@ -813,15 +828,15 @@ export default class MediaDbPlugin extends Plugin { success: downloaded, errors: failed, elapsedMs: Date.now() - startTime, - notes: failed > 0 ? ['Some images could not be downloaded. Check the console for details.'] : [], + notes: failed > 0 ? ['Some images could not be downloaded. A detailed report file has been created in your vault folder.'] : [], }).open(); } /** * Downloads images for a single file. - * @returns true if downloaded, false if failed, null if nothing to download + * @returns object detailing success, possible errors, or whether it was skipped */ - async downloadImagesInFile(file: TFile, silent: boolean = false): Promise { + async downloadImagesInFile(file: TFile, silent: boolean = false): Promise<{ success: boolean; skipped?: boolean; error?: string }> { const metadata = this.getMetadataFromFileCache(file); if (typeof metadata.image === 'string' && metadata.image.startsWith('http')) { try { @@ -841,15 +856,15 @@ export default class MediaDbPlugin extends Plugin { frontmatter.image = `[[${imagePath}]]`; }); if (!silent) new Notice(`MDB | Image downloaded for ${file.basename}`); - return true; + return { success: true }; } catch (e) { console.error("MDB | Image download failed for", file.path, e); if (!silent) new Notice(`MDB | Image download failed for ${file.basename}`); - return false; + return { success: false, error: `${e}` }; } } if (!silent) new Notice(`MDB | No external image found in ${file.basename}`); - return null; + return { success: false, skipped: true }; } private metadataRecordForNewNote(mediaTypeModel: MediaTypeModel): Record { diff --git a/src/utils/AutoTrackerHelper.ts b/src/utils/AutoTrackerHelper.ts index 2e22ca5f..7155c9dd 100644 --- a/src/utils/AutoTrackerHelper.ts +++ b/src/utils/AutoTrackerHelper.ts @@ -1,6 +1,7 @@ import { Notice, TFile, TFolder } from 'obsidian'; import type MediaDbPlugin from 'src/main'; import { CompletionModal } from 'src/modals/CompletionModal'; +import { dateTimeToString, markdownTable } from './Utils'; export class AutoTrackerHelper { readonly plugin: MediaDbPlugin; @@ -53,6 +54,7 @@ export class AutoTrackerHelper { const startTime = Date.now(); let successCount = 0; let failCount = 0; + const erroredFiles: { filePath: string, error: string }[] = []; for (const file of filesToUpdate) { try { @@ -61,11 +63,20 @@ export class AutoTrackerHelper { } catch (e) { console.warn(`MDB Tracker | Failed to auto-update ${file.path}: `, e); failCount++; + erroredFiles.push({ filePath: file.path, error: `${e}` }); } // Sleep longer (1s) to be completely safe during background checks await new Promise(resolve => setTimeout(resolve, 1000)); } + if (failCount > 0 && erroredFiles.length > 0) { + const title = `MDB - auto tracker error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } + new CompletionModal(this.plugin.app, { title: 'Auto Tracker Complete', icon: '🎯', @@ -73,7 +84,7 @@ export class AutoTrackerHelper { success: successCount, errors: failCount, elapsedMs: Date.now() - startTime, - notes: failCount > 0 ? ['Some notes could not be updated. Check the console for details.'] : [], + notes: failCount > 0 ? ['Some notes could not be updated. A detailed report file has been created in your vault folder.'] : [], }).open(); } } diff --git a/src/utils/BulkUpdateHelper.ts b/src/utils/BulkUpdateHelper.ts index 5dcc869b..d5c37db3 100644 --- a/src/utils/BulkUpdateHelper.ts +++ b/src/utils/BulkUpdateHelper.ts @@ -2,6 +2,7 @@ import { TFolder, TFile, Notice } from 'obsidian'; import type MediaDbPlugin from 'src/main'; import { BulkUpdateConfirmModal } from 'src/modals/BulkUpdateConfirmModal'; import { CompletionModal } from 'src/modals/CompletionModal'; +import { dateTimeToString, markdownTable } from './Utils'; export class BulkUpdateHelper { readonly plugin: MediaDbPlugin; @@ -27,6 +28,7 @@ export class BulkUpdateHelper { const startTime = Date.now(); let successCount = 0; let failCount = 0; + const erroredFiles: { filePath: string, error: string }[] = []; for (const file of mediaFiles) { try { @@ -35,10 +37,19 @@ export class BulkUpdateHelper { } catch (e) { console.error(`MDB | Failed to bulk update ${file.path}: `, e); failCount++; + erroredFiles.push({ filePath: file.path, error: `${e}` }); } await new Promise(resolve => setTimeout(resolve, 800)); } + if (failCount > 0 && erroredFiles.length > 0) { + const title = `MDB - bulk update error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } + new CompletionModal(this.plugin.app, { title: 'Bulk Update Complete', icon: '🔄', @@ -46,7 +57,7 @@ export class BulkUpdateHelper { success: successCount, errors: failCount, elapsedMs: Date.now() - startTime, - notes: failCount > 0 ? ['Some files could not be updated. Check the console for details.'] : [], + notes: failCount > 0 ? ['Some files could not be updated. A detailed report file has been created in your vault folder.'] : [], }).open(); }).open(); } From 3860c37e01c9c3b9f196fe1e05ea2958ebcdb1d4 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:50:18 +0300 Subject: [PATCH 41/60] change: frontmatter property ordering Fixed attachTemplate merge strategy: template properties no longer override or reorder API fields. Template-only keys are now appended after all API data. Added explicit tags-to-bottom logic so tags always appears as the last frontmatter property regardless of merge order. --- src/main.ts | 148 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 55 deletions(-) diff --git a/src/main.ts b/src/main.ts index 8705b47c..04f0d17e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,6 @@ import { MarkdownView, Notice, parseYaml, Plugin, stringifyYaml, TFolder, TFile import { requestUrl, normalizePath } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import { APIManager } from './api/APIManager'; -import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './api/musicBrainzConstants'; import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { ComicVineAPI } from './api/apis/ComicVineAPI'; import { GiantBombAPI } from './api/apis/GiantBombAPI'; @@ -22,10 +21,11 @@ import { TMDBSeriesAPI } from './api/apis/TMDBSeriesAPI'; import { VNDBAPI } from './api/apis/VNDBAPI'; import { WikipediaAPI } from './api/apis/WikipediaAPI'; import { GeniusClient } from './api/GeniusClient'; +import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './api/musicBrainzConstants'; import { SpotifyClient } from './api/SpotifyClient'; -import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import { BulkUpdateConfirmModal } from './modals/BulkUpdateConfirmModal'; import { CompletionModal } from './modals/CompletionModal'; +import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import type { ArtistModel } from './models/ArtistModel'; @@ -43,11 +43,11 @@ import { BulkImportHelper } from './utils/BulkImportHelper'; import { BulkUpdateHelper } from './utils/BulkUpdateHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; -import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; -import type { CreateNoteOptions } from './utils/Utils'; import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; +import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; +import type { CreateNoteOptions } from './utils/Utils'; import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable } from './utils/Utils'; import 'src/styles.css'; @@ -132,16 +132,34 @@ export default class MediaDbPlugin extends Plugin { if (typeof item.setSubmenu === 'function') { // @ts-ignore const sub = item.setSubmenu(); - sub.addItem((subItem: any) => subItem.setTitle('Bulk Import Folder').setIcon('database').onClick(() => this.bulkImportHelper.import(file))); - sub.addItem((subItem: any) => subItem.setTitle('Bulk Update Metadata').setIcon('refresh-cw').onClick(() => this.bulkUpdateHelper.updateFolder(file))); - sub.addItem((subItem: any) => subItem.setTitle('Start Auto-Tracker in Folder').setIcon('sync').onClick(() => { - new BulkUpdateConfirmModal( - this.app, - (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); - }).open(); - })); - sub.addItem((subItem: any) => subItem.setTitle('Download images in folder').setIcon('image').onClick(() => this.downloadImagesInFolder(file))); + sub.addItem((subItem: any) => + subItem + .setTitle('Bulk Import Folder') + .setIcon('database') + .onClick(() => this.bulkImportHelper.import(file)), + ); + sub.addItem((subItem: any) => + subItem + .setTitle('Bulk Update Metadata') + .setIcon('refresh-cw') + .onClick(() => this.bulkUpdateHelper.updateFolder(file)), + ); + sub.addItem((subItem: any) => + subItem + .setTitle('Start Auto-Tracker in Folder') + .setIcon('sync') + .onClick(() => { + new BulkUpdateConfirmModal(this.app, (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); + }).open(); + }), + ); + sub.addItem((subItem: any) => + subItem + .setTitle('Download images in folder') + .setIcon('image') + .onClick(() => this.downloadImagesInFolder(file)), + ); } else { // Fallback if setSubmenu isn't in older Obsidian versions item.onClick(() => this.bulkUpdateHelper.updateFolder(file)); @@ -189,7 +207,7 @@ export default class MediaDbPlugin extends Plugin { name: 'Media DB: Download images in active note', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') return false; + if (activeFile?.extension !== 'md') return false; if (!checking) void this.downloadImagesInFile(activeFile); return true; }, @@ -797,7 +815,7 @@ export default class MediaDbPlugin extends Plugin { const startTime = Date.now(); let downloaded = 0; let failed = 0; - const erroredFiles: { filePath: string, error: string }[] = []; + const erroredFiles: { filePath: string; error: string }[] = []; for (const file of files) { const result = await this.downloadImagesInFile(file, true); @@ -809,7 +827,7 @@ export default class MediaDbPlugin extends Plugin { } // wait slightly as anti-rate limit if (!result.skipped) { - await new Promise(r => setTimeout(r, 600)); + await new Promise(r => setTimeout(r, 600)); } } @@ -841,7 +859,7 @@ export default class MediaDbPlugin extends Plugin { if (typeof metadata.image === 'string' && metadata.image.startsWith('http')) { try { const imageUrl = metadata.image; - const extMatch = imageUrl.split('?')[0].match(/\.([a-zA-Z0-9]+)$/); + const extMatch = /\.([a-zA-Z0-9]+)$/.exec(imageUrl.split('?')[0]); const ext = extMatch ? extMatch[1] : 'jpg'; const imgName = replaceIllegalFileNameCharactersInString(file.basename) + '.' + ext; const imgFolder = await this.ensureVaultFolder(this.settings.imageFolder); @@ -858,7 +876,7 @@ export default class MediaDbPlugin extends Plugin { if (!silent) new Notice(`MDB | Image downloaded for ${file.basename}`); return { success: true }; } catch (e) { - console.error("MDB | Image download failed for", file.path, e); + console.error('MDB | Image download failed for', file.path, e); if (!silent) new Notice(`MDB | Image download failed for ${file.basename}`); return { success: false, error: `${e}` }; } @@ -897,9 +915,7 @@ export default class MediaDbPlugin extends Plugin { mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - let fileMetadata: Record = this.modelPropertyMapper.convertObject( - this.metadataRecordForNewNote(mediaTypeModel), - ); + let fileMetadata: Record = this.modelPropertyMapper.convertObject(this.metadataRecordForNewNote(mediaTypeModel)); let fileContent = ''; template = options.attachTemplate ? template : ''; @@ -908,7 +924,10 @@ export default class MediaDbPlugin extends Plugin { ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- - const entityWikiProps = this.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + const entityWikiProps = this.settings.autoTagEntities + .split(',') + .map(s => s.trim().toLowerCase()) + .filter(s => s !== ''); if (entityWikiProps.length > 0) { const folderPrefix = this.settings.wikiFolder ? `${this.settings.wikiFolder}/` : ''; const isEnabled = this.settings.enableWikiLinkParsing; @@ -932,9 +951,12 @@ export default class MediaDbPlugin extends Plugin { } // --- Auto-Tag Logic --- - const tagProps = this.settings.autoTagProperties.split(',').map(s => s.trim().toLowerCase()).filter(s => s !== ''); + const tagProps = this.settings.autoTagProperties + .split(',') + .map(s => s.trim().toLowerCase()) + .filter(s => s !== ''); if (this.settings.enableAutoTagging && tagProps.length > 0) { - const existingTags: string[] = Array.isArray(fileMetadata['tags']) ? (fileMetadata['tags'] as string[]) : []; + const existingTags: string[] = Array.isArray(fileMetadata.tags) ? (fileMetadata.tags as string[]) : []; const newTags = new Set(existingTags.filter(t => typeof t === 'string' && t.trim() !== '')); for (const [key, value] of Object.entries(fileMetadata)) { @@ -951,25 +973,32 @@ export default class MediaDbPlugin extends Plugin { .replace(/\s+/g, '-') .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') .toLowerCase(); - + if (sanitized) newTags.add(sanitized); } } } } - + if (newTags.size > 0) { - fileMetadata['tags'] = Array.from(newTags); + fileMetadata.tags = Array.from(newTags); } } if (mediaTypeModel.getMediaType() === MediaType.Song) { const song = mediaTypeModel as SongModel; - if(song.lyrics.length > 0) { + if (song.lyrics.length > 0) { fileContent += `# Lyrics\n\`\`\`\n${song.lyrics}\n\`\`\`\n`; } } + // Ensure 'tags' always appears last in frontmatter + if ('tags' in fileMetadata) { + const tagsValue = fileMetadata['tags']; + delete fileMetadata['tags']; + fileMetadata['tags'] = tagsValue; + } + if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { // Include the media variable in all templater commands by using a top level JavaScript execution command. const mediaJson = JSON.stringify(mediaTypeModel, (key, value: unknown) => (key === 'lyrics' ? undefined : value)); @@ -982,11 +1011,14 @@ export default class MediaDbPlugin extends Plugin { } extractManualTags(metadata: Record, autoTagKeys: string): string[] { - const allTagsRaw = metadata['tags']; + const allTagsRaw = metadata.tags; const allTags = Array.isArray(allTagsRaw) ? allTagsRaw : typeof allTagsRaw === 'string' ? [allTagsRaw] : []; if (allTags.length === 0) return []; - - const tagProps = autoTagKeys.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + + const tagProps = autoTagKeys + .split(',') + .map(s => s.trim().toLowerCase()) + .filter(s => s); const autoTagValues = new Set(); for (const [key, value] of Object.entries(metadata)) { @@ -996,7 +1028,11 @@ export default class MediaDbPlugin extends Plugin { if (typeof v === 'string') { let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); if (clean.includes('|')) clean = clean.split('|')[1]; - const sanitized = clean.trim().replace(/\s+/g, '-').replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '').toLowerCase(); + const sanitized = clean + .trim() + .replace(/\s+/g, '-') + .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') + .toLowerCase(); if (sanitized) autoTagValues.add(sanitized); } } @@ -1012,11 +1048,11 @@ export default class MediaDbPlugin extends Plugin { } const attachFileMetadata = this.getMetadataFromFileCache(fileToAttach); - + // Rescue arrays that Object.assign would normally crush const rescueArray = (key: string) => { const arr = attachFileMetadata[key]; - if (Array.isArray(arr)) return [...arr as string[]]; + if (Array.isArray(arr)) return [...(arr as string[])]; if (typeof arr === 'string' && arr.trim()) return [arr]; return []; }; @@ -1027,14 +1063,16 @@ export default class MediaDbPlugin extends Plugin { fileMetadata = Object.assign(attachFileMetadata, fileMetadata); // Merge tags cleanly (Preserving only manual user tags, discarding old ghost auto-tags!) - const newObjTags = fileMetadata['tags']; + const newObjTags = fileMetadata.tags; const finalTags = new Set([...oldManualTags, ...(Array.isArray(newObjTags) ? newObjTags : typeof newObjTags === 'string' ? [newObjTags] : [])].map(t => String(t).trim())); - if (finalTags.size > 0) fileMetadata['tags'] = Array.from(finalTags); + if (finalTags.size > 0) fileMetadata.tags = Array.from(finalTags); // Merge aliases cleanly - const newObjAliases = fileMetadata['aliases']; - const finalAliases = new Set([...oldAliases, ...(Array.isArray(newObjAliases) ? newObjAliases : typeof newObjAliases === 'string' ? [newObjAliases] : [])].map(a => String(a).trim())); - if (finalAliases.size > 0) fileMetadata['aliases'] = Array.from(finalAliases); + const newObjAliases = fileMetadata.aliases; + const finalAliases = new Set( + [...oldAliases, ...(Array.isArray(newObjAliases) ? newObjAliases : typeof newObjAliases === 'string' ? [newObjAliases] : [])].map(a => String(a).trim()), + ); + if (finalAliases.size > 0) fileMetadata.aliases = Array.from(finalAliases); let attachFileContent: string = await this.app.vault.read(fileToAttach); const regExp = new RegExp(this.frontMatterRexExpPattern); @@ -1051,8 +1089,12 @@ export default class MediaDbPlugin extends Plugin { } const templateMetadata = this.getMetaDataFromFileContent(template); - // TODO: better object merging - fileMetadata = Object.assign(templateMetadata, fileMetadata); + // Merge: API data wins and stays at top; template-only keys are appended at the bottom + for (const [key, value] of Object.entries(templateMetadata)) { + if (!(key in fileMetadata)) { + fileMetadata[key] = value; + } + } const regExp = new RegExp(this.frontMatterRexExpPattern); const attachFileContent = template.replace(regExp, ''); @@ -1150,13 +1192,11 @@ export default class MediaDbPlugin extends Plugin { if (!this._ribbonEl) { this._ribbonEl = this.addRibbonIcon('sync', 'Media DB: Auto-Tracker Sync', () => { if (this.autoTrackerHelper.isScanning) { - new Notice("Auto-Tracker is currently syncing in the background."); + new Notice('Auto-Tracker is currently syncing in the background.'); } else { - new BulkUpdateConfirmModal( - this.app, - (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate); - }).open(); + new BulkUpdateConfirmModal(this.app, (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate); + }).open(); } }); this._ribbonEl.addClass('obsidian-media-db-plugin-ribbon-class'); @@ -1182,7 +1222,6 @@ export default class MediaDbPlugin extends Plugin { } async updateNote(activeFile: TFile, onlyMetadata: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { - let metadata = this.getMetadataFromFileCache(activeFile); metadata = this.modelPropertyMapper.convertObjectBack(metadata); @@ -1197,10 +1236,7 @@ export default class MediaDbPlugin extends Plugin { throw new Error('MDB | active note type is not recognized; check Settings → Note type for each media kind'); } let dataSource = typeof metadata.dataSource === 'string' ? metadata.dataSource.trim() : ''; - if ( - !dataSource && - musicBrainzRegisteredApiName(mediaType) - ) { + if (!dataSource && musicBrainzRegisteredApiName(mediaType)) { dataSource = MUSICBRAINZ_NOTE_DATA_SOURCE; } if (!dataSource) { @@ -1249,8 +1285,10 @@ export default class MediaDbPlugin extends Plugin { if (anyLoaded.bandFolder && !loadedSettings.artistFolder) loadedSettings.artistFolder = anyLoaded.bandFolder; if (anyLoaded.bandFileNameTemplate && !loadedSettings.artistFileNameTemplate) loadedSettings.artistFileNameTemplate = anyLoaded.bandFileNameTemplate; if (anyLoaded.bandNoteType && !loadedSettings.artistNoteType) loadedSettings.artistNoteType = anyLoaded.bandNoteType; - if (anyLoaded.bandUseFileTreeForSongs !== undefined && loadedSettings.artistUseFileTreeForSongs === false) loadedSettings.artistUseFileTreeForSongs = anyLoaded.bandUseFileTreeForSongs; - if (anyLoaded.MusicBrainzBandAPI_disabledMediaTypes && !loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes) loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes = anyLoaded.MusicBrainzBandAPI_disabledMediaTypes; + if (anyLoaded.bandUseFileTreeForSongs !== undefined && loadedSettings.artistUseFileTreeForSongs === false) + loadedSettings.artistUseFileTreeForSongs = anyLoaded.bandUseFileTreeForSongs; + if (anyLoaded.MusicBrainzBandAPI_disabledMediaTypes && !loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes) + loadedSettings.MusicBrainzArtistAPI_disabledMediaTypes = anyLoaded.MusicBrainzBandAPI_disabledMediaTypes; } this.settings = loadedSettings; From 53418fb288d4156681f99ee94363bfca4e7e6d5e Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:55:17 +0300 Subject: [PATCH 42/60] Add files via upload --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 7cd31f37..72a6dc6b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,47 @@ Then the plugin will go through every file in the folder and prompt you to selec 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. +#### Intelligent Wiki-Link Generation + +Automatically format specific metadata fields into Obsidian Wiki-Links based on a customizable whitelist. + +- You can specify comma-separated properties (like `genres, publishers, storefront`) in the plugin settings. +- The plugin will parse both API-fetched arrays and your own custom manual properties, formatting them safely as `[[folder/value|value]]`. +- If you disable the feature later, updating the note will cleanly strip the brackets and revert the entries back to plain text. + +#### Auto-Tagging Engine & Ghost Tag Purging + +Generate clean, sanitized hashtags automatically (e.g. `#role-playing-rpg`) from mapped array properties or Wiki-Links. + +- The engine creates tags without destroying any manual tags you have explicitly typed into your notes. +- **Ghost Tag Purging**: Whenever a piece of media updates (e.g. a game's genre changes on IGDB), the plugin intelligently calculates which old tags it previously generated and safely removes them, leaving your manual user tags perfectly preserved. + +#### Background Auto-Tracker + +An automated, non-blocking background scanner that actively searches your vault for ongoing series (Airing) or unreleased games/movies and queues them for silent metadata updates. + +- Scans trigger once on Obsidian startup or can be triggered manually from the left ribbon. +- Includes a sophisticated **Emergency Abort Mechanism**: At any point during a bulk process or background tracker queue, you can click the Ribbon Icon or use the `Cancel All Updates` button inside the Overwrite Modal to instantly halt the entire queue gracefully without restarting Obsidian. + +The plugin allows you to import your preexisting media collection and upgrade it to Media DB entries. + +##### Prerequisites + +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. + +##### Importing + +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. + +Then the plugin will go through every file in the folder and prompt you to select from the search results. + +##### 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. + ### How to install **The plugin is now released, so it can be installed directly through Obsidian's plugin installer.** @@ -130,6 +171,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 From 89e4b051f9546f997a519c292cd97ecf4ed6d9b0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:18:30 +0300 Subject: [PATCH 43/60] Add files via upload From 35c8fd5ae93c0eeb1947e9a40cc0f420e0d9ba6d Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:20:25 +0300 Subject: [PATCH 44/60] Add files via upload --- src/api/APIManager.ts | 11 +- src/api/APIModel.ts | 5 +- src/api/GeniusClient.ts | 2 - src/api/SpotifyClient.ts | 1 - src/api/apis/BoardGameGeekAPI.ts | 4 +- src/api/apis/GiantBombAPI.ts | 2 +- src/api/apis/IGDBAPI.ts | 126 ++-- src/api/apis/MobyGamesAPI.ts | 2 +- src/api/apis/MusicBrainzAPI.ts | 27 +- src/api/apis/MusicBrainzArtistAPI.ts | 4 +- src/api/apis/RAWGAPI.ts | 60 +- src/api/apis/TMDBMovieAPI.ts | 11 +- src/api/apis/TMDBSeasonAPI.ts | 4 +- src/api/apis/TMDBSeriesAPI.ts | 2 +- src/api/apis/VNDBAPI.ts | 4 +- src/api/geniusLyricsExtract.ts | 10 +- src/main.ts | 406 ++++++++++--- src/modals/BulkRecreateConfirmModal.ts | 71 +++ src/modals/BulkUpdateConfirmModal.ts | 17 +- src/modals/CompletionModal.ts | 3 +- src/modals/ConfirmOverwriteModal.ts | 118 +++- src/modals/MediaDbPreviewModal.ts | 4 + src/modals/MediaDbSearchResultModal.ts | 85 ++- src/modals/MediaDbSeasonSelectModal.ts | 40 +- src/modals/PropertyMappingModal.ts | 4 +- src/models/MediaTypeModel.ts | 22 +- src/settings/PropertyMapper.ts | 209 +++++-- src/settings/PropertyMapping.ts | 574 +++++++++--------- .../PropertyMappingModelComponent.tsx | 352 +++++++---- src/settings/Settings.ts | 177 +++--- src/settings/apiSecretsHelper.ts | 26 +- src/styles.css | 123 +++- src/utils/AutoTrackerHelper.ts | 11 +- src/utils/BulkImportHelper.ts | 2 +- src/utils/BulkRecreateHelper.ts | 69 +++ src/utils/BulkUpdateHelper.ts | 7 +- src/utils/MediaTypeManager.ts | 19 +- src/utils/Utils.ts | 12 + src/utils/noteTypeSettings.ts | 7 +- 39 files changed, 1802 insertions(+), 831 deletions(-) create mode 100644 src/modals/BulkRecreateConfirmModal.ts create mode 100644 src/utils/BulkRecreateHelper.ts diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index bcdd5401..76ffb171 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -1,12 +1,8 @@ import { Notice } from 'obsidian'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaType } from '../utils/MediaType'; -import { - isMusicBrainzFamilyDataSource, - musicBrainzRegisteredApiName, - MUSICBRAINZ_NOTE_DATA_SOURCE, -} from './musicBrainzConstants'; import type { APIModel } from './APIModel'; +import { isMusicBrainzFamilyDataSource, musicBrainzRegisteredApiName, MUSICBRAINZ_NOTE_DATA_SOURCE } from './musicBrainzConstants'; export class APIManager { apis: APIModel[]; @@ -59,10 +55,7 @@ export class APIManager { */ async queryDetailedInfoById(id: string, apiName: string, mediaType?: MediaType): Promise { const trimmed = apiName.trim(); - const effectiveApiName = - trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) - ? MUSICBRAINZ_NOTE_DATA_SOURCE - : trimmed || apiName; + const effectiveApiName = trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) ? MUSICBRAINZ_NOTE_DATA_SOURCE : trimmed || apiName; if (isMusicBrainzFamilyDataSource(effectiveApiName) && mediaType !== undefined) { const registeredName = musicBrainzRegisteredApiName(mediaType); diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index 50e6d72c..d15fdcb0 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -40,7 +40,10 @@ export abstract class APIModel { * @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/') */ wikilinkValueFor(_property: string, value: string, _obj: Record, folderPrefix: string): string { - const clean = value.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; + const clean = value + .replace(/^\[\[(.*?)\]\]$/, '$1') + .split('|') + .pop()!; return `[[${folderPrefix}${clean}|${clean}]]`; } } diff --git a/src/api/GeniusClient.ts b/src/api/GeniusClient.ts index 4b228177..eb081dbd 100644 --- a/src/api/GeniusClient.ts +++ b/src/api/GeniusClient.ts @@ -1,7 +1,5 @@ import { requestUrl } from 'obsidian'; - import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; - import { extractLyricsFromGeniusHtml } from './geniusLyricsExtract'; interface GeniusSearchHit { diff --git a/src/api/SpotifyClient.ts b/src/api/SpotifyClient.ts index 48e87bae..a2a468e2 100644 --- a/src/api/SpotifyClient.ts +++ b/src/api/SpotifyClient.ts @@ -1,5 +1,4 @@ import { requestUrl } from 'obsidian'; - import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; interface SpotifyTokenResponse { diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index bbe251d5..704d54fa 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -1,10 +1,10 @@ import { requestUrl } from 'obsidian'; import { BoardGameModel } from 'src/models/BoardGameModel'; import type MediaDbPlugin from '../../main'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; -import { coerceYear } from '../../utils/Utils'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; +import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; // sadly no open api schema available diff --git a/src/api/apis/GiantBombAPI.ts b/src/api/apis/GiantBombAPI.ts index 3aba1336..99db26e5 100644 --- a/src/api/apis/GiantBombAPI.ts +++ b/src/api/apis/GiantBombAPI.ts @@ -2,8 +2,8 @@ import createClient from 'openapi-fetch'; import { coerceYear, obsidianFetch } from 'src/utils/Utils'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; import type { paths } from '../schemas/GiantBomb'; diff --git a/src/api/apis/IGDBAPI.ts b/src/api/apis/IGDBAPI.ts index 51306143..89355bc4 100644 --- a/src/api/apis/IGDBAPI.ts +++ b/src/api/apis/IGDBAPI.ts @@ -7,21 +7,49 @@ import { MediaType } from '../../utils/MediaType'; import { coerceYear } from '../../utils/Utils'; import { APIModel } from '../APIModel'; -interface IGDBCover { url: string; } -interface IGDBGenre { name: string; } -interface IGDBCompany { name: string; } -interface IGDBInvolvedCompany { company: IGDBCompany; developer: boolean; publisher: boolean; } -interface IGDBPlatform { name: string; } -interface IGDBGameMode { name: string; } -interface IGDBCollection { name: string; } +interface IGDBCover { + url: string; +} +interface IGDBGenre { + name: string; +} +interface IGDBCompany { + name: string; +} +interface IGDBInvolvedCompany { + company: IGDBCompany; + developer: boolean; + publisher: boolean; +} +interface IGDBPlatform { + name: string; +} +interface IGDBGameMode { + name: string; +} +interface IGDBCollection { + name: string; +} interface IGDBGame { - id: number; name: string; cover?: IGDBCover; first_release_date?: number; - summary?: string; total_rating?: number; url?: string; - genres?: IGDBGenre[]; involved_companies?: IGDBInvolvedCompany[]; - platforms?: IGDBPlatform[]; game_modes?: IGDBGameMode[]; - collection?: IGDBCollection; collections?: IGDBCollection[]; franchises?: IGDBCollection[]; + id: number; + name: string; + cover?: IGDBCover; + first_release_date?: number; + summary?: string; + total_rating?: number; + url?: string; + genres?: IGDBGenre[]; + involved_companies?: IGDBInvolvedCompany[]; + platforms?: IGDBPlatform[]; + game_modes?: IGDBGameMode[]; + collection?: IGDBCollection; + collections?: IGDBCollection[]; + franchises?: IGDBCollection[]; +} +interface TwitchAuthResponse { + access_token: string; + expires_in: number; } -interface TwitchAuthResponse { access_token: string; expires_in: number; } export class IGDBAPI extends APIModel { plugin: MediaDbPlugin; @@ -55,7 +83,7 @@ export class IGDBAPI extends APIModel { if (response.status !== 200) throw Error(`MDB | Auth failed for ${this.apiName}. Check Credentials.`); const data = response.json as TwitchAuthResponse; this.accessToken = data.access_token; - this.tokenExpiry = currentTime + (data.expires_in * 1000) - 60000; + this.tokenExpiry = currentTime + data.expires_in * 1000 - 60000; return this.accessToken; } @@ -65,19 +93,25 @@ export class IGDBAPI extends APIModel { const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); const queryBody = `search "${title}"; fields name, cover.url, first_release_date, summary, total_rating; limit 20;`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; return data.map(result => { const year = result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0; const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, year: coerceYear(year), - dataSource: this.apiName, id: result.id.toString(), image: image + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: coerceYear(year), + dataSource: this.apiName, + id: result.id.toString(), + image: image, }); }); } @@ -88,16 +122,17 @@ export class IGDBAPI extends APIModel { const clientId = getApiSecretValue(this.plugin.app, this.plugin.settings.linkedApiSecretIds, ApiSecretID.igdbClientId); const queryBody = `fields name, cover.url, first_release_date, summary, total_rating, url, genres.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher, platforms.name, game_modes.name, collection.name, collections.name, franchises.name; where id = ${id};`; const response = await requestUrl({ - url: `${this.apiUrl}/games`, method: 'POST', - headers: { 'Client-ID': clientId, 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }, + url: `${this.apiUrl}/games`, + method: 'POST', + headers: { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json' }, body: queryBody, }); if (response.status !== 200) throw Error(`MDB | Received status code ${response.status} from ${this.apiName}.`); - + const data = response.json as IGDBGame[]; if (!data || data.length === 0) throw Error(`MDB | No result found for ID ${id}`); const result = data[0]; - + const developers: string[] = []; const publishers: string[] = []; result.involved_companies?.forEach(c => { @@ -107,31 +142,44 @@ export class IGDBAPI extends APIModel { const dateStr = result.first_release_date ? new Date(result.first_release_date * 1000).toISOString().split('T')[0] : ''; const image = result.cover?.url ? 'https:' + result.cover.url.replace('t_thumb', 't_1080p').replace(/\.jpg$/, '.webp') : ''; - let combinedSeries: string[] = []; + const combinedSeries: string[] = []; // Öncelik 1: Franchise (Ana marka) - result.franchises?.forEach(f => { if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); }); - + result.franchises?.forEach(f => { + if (f.name && !combinedSeries.includes(f.name)) combinedSeries.push(f.name); + }); + // Öncelik 2: Franchise yoksa Collection (Seri) fallback'i if (combinedSeries.length === 0) { if (result.collection?.name) combinedSeries.push(result.collection.name); - result.collections?.forEach(c => { if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); }); + result.collections?.forEach(c => { + if (c.name && !combinedSeries.includes(c.name)) combinedSeries.push(c.name); + }); } return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, - year: coerceYear( - result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0, - ), - dataSource: this.apiName, url: result.url, id: result.id.toString(), - summary: result.summary ?? '', series: combinedSeries, - gameModes: result.game_modes?.map(g => g.name) || [], platforms: result.platforms?.map(p => p.name) || [], - developers: developers, publishers: publishers, genres: result.genres?.map(g => g.name) || [], - onlineRating: result.total_rating ? Math.round(result.total_rating * 10) / 10 : 0, image: image, - released: result.first_release_date ? (result.first_release_date * 1000) <= Date.now() : false, + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: coerceYear(result.first_release_date ? new Date(result.first_release_date * 1000).getFullYear() : 0), + dataSource: this.apiName, + url: result.url, + id: result.id.toString(), + summary: result.summary ?? '', + series: combinedSeries, + gameModes: result.game_modes?.map(g => g.name) || [], + platforms: result.platforms?.map(p => p.name) || [], + developers: developers, + publishers: publishers, + genres: result.genres?.map(g => g.name) || [], + onlineRating: result.total_rating ? Math.round(result.total_rating * 10) / 10 : 0, + image: image, + released: result.first_release_date ? result.first_release_date * 1000 <= Date.now() : false, releaseDate: dateStr ? this.plugin.dateFormatter.format(dateStr, this.apiDateFormat) : '', userData: { played: false, personalRating: 0 }, }); } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; } -} \ No newline at end of file + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.IGDBAPI_disabledMediaTypes || []; + } +} diff --git a/src/api/apis/MobyGamesAPI.ts b/src/api/apis/MobyGamesAPI.ts index b201ad03..5fa9853a 100644 --- a/src/api/apis/MobyGamesAPI.ts +++ b/src/api/apis/MobyGamesAPI.ts @@ -3,8 +3,8 @@ import { requestUrl } from 'obsidian'; import type MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index df6d0d16..7732fabe 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -104,7 +104,7 @@ export class MusicBrainzAPI extends APIModel { this.plugin = plugin; this.apiName = 'MusicBrainz API'; - this.apiDescription = 'Free API for music albums.'; + this.apiDescription = 'Free API for music releases.'; this.apiUrl = 'https://musicbrainz.org/'; this.types = [MediaType.MusicRelease]; } @@ -198,7 +198,7 @@ export class MusicBrainzAPI extends APIModel { const releaseData = (await releaseResponse.json) as MediaResponse; const tracks = extractTracksFromMedia(releaseData.media); - // Calculate total album length for the first release + // Calculate total length for the first release const totalrawLength = releaseData.media[0]?.tracks.reduce((sum, track) => { const len = track.length ?? track.recording?.length; @@ -235,29 +235,6 @@ export class MusicBrainzAPI extends APIModel { }, }); } - /** - * For the 'albumTitle' property (used on Song notes), the link target is - * derived from the MusicRelease file name template rather than the raw title. - */ - override wikilinkValueFor(property: string, value: string, obj: Record, folderPrefix: string): string { - if (property === 'albumTitle') { - const title = value.trim(); - const artistsRaw = obj.artists; - const artists = Array.isArray(artistsRaw) - ? artistsRaw.filter((a): a is string => typeof a === 'string') - : []; - const releaseModel = new MusicReleaseModel({ - type: 'musicRelease', title, englishTitle: title, - year: coerceYear(obj.year), releaseDate: '', dataSource: '', - url: '', id: '', image: '', artists, genres: [], - subType: 'album', language: '', rating: 0, - }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); - return linkTarget === title ? `[[${linkTarget}]]` : `[[${linkTarget}|${title}]]`; - } - return super.wikilinkValueFor(property, value, obj, folderPrefix); - } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.MusicBrainzAPI_disabledMediaTypes; } diff --git a/src/api/apis/MusicBrainzArtistAPI.ts b/src/api/apis/MusicBrainzArtistAPI.ts index 397b6c81..151c51fa 100644 --- a/src/api/apis/MusicBrainzArtistAPI.ts +++ b/src/api/apis/MusicBrainzArtistAPI.ts @@ -84,7 +84,7 @@ export class MusicBrainzArtistAPI extends APIModel { this.plugin = plugin; this.apiName = 'MusicBrainz Artist API'; - this.apiDescription = 'MusicBrainz artist search and studio album discography.'; + this.apiDescription = 'MusicBrainz artist search and studio release discography.'; this.apiUrl = 'https://musicbrainz.org/'; this.types = [MediaType.Artist]; } @@ -191,7 +191,7 @@ export class MusicBrainzArtistAPI extends APIModel { } /** - * Lists release group MBIDs for studio albums (primary type album, excluding live/compilations/etc.). + * Lists release group MBIDs for studio releases (MusicBrainz primary type album, excluding live/compilations/etc.). * Passes release-group-status=website-default so MusicBrainz omits groups that only have bootleg, promotional, or pseudo-releases * (see MusicBrainz API “Release (Group) Type and Status”). */ diff --git a/src/api/apis/RAWGAPI.ts b/src/api/apis/RAWGAPI.ts index c22c9a9a..2ab2dd97 100644 --- a/src/api/apis/RAWGAPI.ts +++ b/src/api/apis/RAWGAPI.ts @@ -7,11 +7,21 @@ import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; interface RAWGGame { - id: number; name: string; released?: string; background_image?: string; - name_original?: string; website?: string; slug?: string; metacritic?: number; - developers?: { name: string }[]; publishers?: { name: string }[]; genres?: { name: string }[]; + id: number; + name: string; + released?: string; + background_image?: string; + name_original?: string; + website?: string; + slug?: string; + metacritic?: number; + developers?: { name: string }[]; + publishers?: { name: string }[]; + genres?: { name: string }[]; +} +interface RAWGSearchResponse { + results: RAWGGame[]; } -interface RAWGSearchResponse { results: RAWGGame[]; } export class RAWGAPI extends APIModel { plugin: MediaDbPlugin; @@ -36,11 +46,18 @@ export class RAWGAPI extends APIModel { if (response.status !== 200) throw Error(`MDB | Error ${response.status} from ${this.apiName}.`); const data = response.json as RAWGSearchResponse; - return data.results.map(result => new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name, - year: result.released ? new Date(result.released).getFullYear() : 0, - dataSource: this.apiName, id: result.id.toString(), image: result.background_image - })); + return data.results.map( + result => + new GameModel({ + type: MediaType.Game, + title: result.name, + englishTitle: result.name, + year: result.released ? new Date(result.released).getFullYear() : 0, + dataSource: this.apiName, + id: result.id.toString(), + image: result.background_image, + }), + ); } async getById(id: string): Promise { @@ -54,15 +71,24 @@ export class RAWGAPI extends APIModel { const result = response.json as RAWGGame; return new GameModel({ - type: MediaType.Game, title: result.name, englishTitle: result.name_original || result.name, + type: MediaType.Game, + title: result.name, + englishTitle: result.name_original || result.name, year: result.released ? new Date(result.released).getFullYear() : 0, - dataSource: this.apiName, url: result.website || `https://rawg.io/games/${result.slug}`, - id: result.id.toString(), developers: result.developers?.map(d => d.name) || [], - publishers: result.publishers?.map(p => p.name) || [], genres: result.genres?.map(g => g.name) || [], - onlineRating: result.metacritic, image: result.background_image, - released: result.released != null, releaseDate: result.released, + dataSource: this.apiName, + url: result.website || `https://rawg.io/games/${result.slug}`, + id: result.id.toString(), + developers: result.developers?.map(d => d.name) || [], + publishers: result.publishers?.map(p => p.name) || [], + genres: result.genres?.map(g => g.name) || [], + onlineRating: result.metacritic, + image: result.background_image, + released: result.released != null, + releaseDate: result.released, userData: { played: false, personalRating: 0 }, }); } - getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; } -} \ No newline at end of file + getDisabledMediaTypes(): MediaType[] { + return this.plugin.settings.RAWGAPI_disabledMediaTypes || []; + } +} diff --git a/src/api/apis/TMDBMovieAPI.ts b/src/api/apis/TMDBMovieAPI.ts index ea0cd19a..133b0145 100644 --- a/src/api/apis/TMDBMovieAPI.ts +++ b/src/api/apis/TMDBMovieAPI.ts @@ -11,16 +11,7 @@ import { APIModel } from '../APIModel'; import type { paths } from '../schemas/TMDB'; /** TMDB `credits.crew` jobs that count as writing credits for movies. */ -const TMDB_WRITING_CREW_JOBS = new Set([ - 'Writer', - 'Screenplay', - 'Story', - 'Teleplay', - 'Original Story', - 'Characters', - 'Novel', - 'Screenstory', -]); +const TMDB_WRITING_CREW_JOBS = new Set(['Writer', 'Screenplay', 'Story', 'Teleplay', 'Original Story', 'Characters', 'Novel', 'Screenstory']); function tmdbWritingCreditsFromCrew(crew: any[] | undefined): string[] { if (!crew?.length) { diff --git a/src/api/apis/TMDBSeasonAPI.ts b/src/api/apis/TMDBSeasonAPI.ts index b91b50a0..5c0eecd4 100644 --- a/src/api/apis/TMDBSeasonAPI.ts +++ b/src/api/apis/TMDBSeasonAPI.ts @@ -3,8 +3,8 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { SeasonModel } from '../../models/SeasonModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; import type { paths } from '../schemas/TMDB'; @@ -99,6 +99,7 @@ export class TMDBSeasonAPI extends APIModel { id: result.id?.toString() ?? '', seasonTitle: result.name ?? result.original_name ?? '', seasonNumber: totalSeasons, + image: result.poster_path ? `https://image.tmdb.org/t/p/w780${result.poster_path}` : '', }), ); } @@ -151,6 +152,7 @@ export class TMDBSeasonAPI extends APIModel { id: `${tvId}/season/${seasonNumber}`, seasonTitle: season.name ?? titleText, seasonNumber: seasonNumber, + image: season.poster_path ?? '', }), ); } diff --git a/src/api/apis/TMDBSeriesAPI.ts b/src/api/apis/TMDBSeriesAPI.ts index 61fe68f3..b8fb8bcf 100644 --- a/src/api/apis/TMDBSeriesAPI.ts +++ b/src/api/apis/TMDBSeriesAPI.ts @@ -3,8 +3,8 @@ import createClient from 'openapi-fetch'; import type MediaDbPlugin from '../../main'; import type { MediaTypeModel } from '../../models/MediaTypeModel'; -import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { SeriesModel } from '../../models/SeriesModel'; +import { ApiSecretID, getApiSecretValue } from '../../settings/apiSecretsHelper'; import { MediaType } from '../../utils/MediaType'; import { APIModel } from '../APIModel'; import type { paths } from '../schemas/TMDB'; diff --git a/src/api/apis/VNDBAPI.ts b/src/api/apis/VNDBAPI.ts index 2a933dae..e1272f69 100644 --- a/src/api/apis/VNDBAPI.ts +++ b/src/api/apis/VNDBAPI.ts @@ -191,9 +191,7 @@ export class VNDBAPI extends APIModel { type: MediaType.Game, title: vn.title, englishTitle: vn.titles.find(t => t.lang === 'en')?.title ?? vn.title, - year: coerceYear( - vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear() : 0, - ), + year: coerceYear(vn.released && vn.released !== 'TBA' ? new Date(vn.released).getFullYear() : 0), dataSource: this.apiName, id: vn.id, }), diff --git a/src/api/geniusLyricsExtract.ts b/src/api/geniusLyricsExtract.ts index 000e9218..ecdc178a 100644 --- a/src/api/geniusLyricsExtract.ts +++ b/src/api/geniusLyricsExtract.ts @@ -1,5 +1,4 @@ -const LYRICS_CONTAINER_OPEN_RE = - /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; +const LYRICS_CONTAINER_OPEN_RE = /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; function stripHtmlToPlainLyrics(fragment: string): string { return fragment @@ -80,8 +79,11 @@ export function extractLyricsFromGeniusHtml(html: string): string { } if (chunks.length === 0) { - return '' + return ''; } - return chunks.join('\n\n').replace(/\n{3,}/g, '\n\n').trim(); + return chunks + .join('\n\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); } diff --git a/src/main.ts b/src/main.ts index 04f0d17e..a1618e7e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,7 @@ import { MUSICBRAINZ_NOTE_DATA_SOURCE, musicBrainzRegisteredApiName } from './ap import { SpotifyClient } from './api/SpotifyClient'; import { BulkUpdateConfirmModal } from './modals/BulkUpdateConfirmModal'; import { CompletionModal } from './modals/CompletionModal'; -import { ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; +import { ConfirmOverwriteChoice, ConfirmOverwriteModal } from './modals/ConfirmOverwriteModal'; import type { SeasonSelectModalElement } from './modals/MediaDbSeasonSelectModal'; import { MediaDbSeasonSelectModal } from './modals/MediaDbSeasonSelectModal'; import type { ArtistModel } from './models/ArtistModel'; @@ -41,14 +41,14 @@ import { getDefaultSettings, MediaDbSettingTab, propertyMappingModelsInDisplayOr import { AutoTrackerHelper } from './utils/AutoTrackerHelper'; import { BulkImportHelper } from './utils/BulkImportHelper'; import { BulkUpdateHelper } from './utils/BulkUpdateHelper'; +import { BulkRecreateHelper } from './utils/BulkRecreateHelper'; import { DateFormatter } from './utils/DateFormatter'; import { MEDIA_TYPES, MediaTypeManager } from './utils/MediaTypeManager'; import type { SearchModalOptions } from './utils/ModalHelper'; import { ModalHelper } from './utils/ModalHelper'; -import { normalizeTitleForAsciiAlias } from './utils/normalizeTitleForAlias'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from './utils/noteTypeSettings'; import type { CreateNoteOptions } from './utils/Utils'; -import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable } from './utils/Utils'; +import { replaceIllegalFileNameCharactersInString, unCamelCase, hasTemplaterPlugin, useTemplaterPluginInFile, dateTimeToString, markdownTable, parseUsdWholeDollarsFromDisplayString, normalizeTitleForAsciiAlias } from './utils/Utils'; import 'src/styles.css'; export type Metadata = Record; @@ -67,6 +67,7 @@ export default class MediaDbPlugin extends Plugin { modalHelper!: ModalHelper; bulkImportHelper!: BulkImportHelper; bulkUpdateHelper!: BulkUpdateHelper; + bulkRecreateHelper!: BulkRecreateHelper; autoTrackerHelper!: AutoTrackerHelper; dateFormatter!: DateFormatter; @@ -99,6 +100,7 @@ export default class MediaDbPlugin extends Plugin { this.modalHelper = new ModalHelper(this); this.bulkImportHelper = new BulkImportHelper(this); this.bulkUpdateHelper = new BulkUpdateHelper(this); + this.bulkRecreateHelper = new BulkRecreateHelper(this); this.autoTrackerHelper = new AutoTrackerHelper(this); this.dateFormatter = new DateFormatter(); @@ -144,6 +146,12 @@ export default class MediaDbPlugin extends Plugin { .setIcon('refresh-cw') .onClick(() => this.bulkUpdateHelper.updateFolder(file)), ); + sub.addItem((subItem: any) => + subItem + .setTitle('Bulk Recreate Notes') + .setIcon('file-stack') + .onClick(() => this.bulkRecreateHelper.recreateFolder(file)), + ); sub.addItem((subItem: any) => subItem .setTitle('Start Auto-Tracker in Folder') @@ -180,6 +188,17 @@ export default class MediaDbPlugin extends Plugin { }, }); + this.addCommand({ + id: 'media-db-bulk-recreate-active-file-folder', + name: 'Media DB: Bulk Recreate Notes (Active Context)', + checkCallback: (checking: boolean) => { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile?.parent) return false; + if (!checking) void this.bulkRecreateHelper.recreateFolder(activeFile.parent); + return true; + }, + }); + this.addCommand({ id: 'media-db-bulk-update-active-file-folder', name: 'Media DB: Bulk Update Metadata (Active Context)', @@ -630,25 +649,146 @@ export default class MediaDbPlugin extends Plugin { return folder; } - private async importArtistDiscography(artist: ArtistModel, options: CreateNoteOptions): Promise { + private async importSongNotesForMusicReleaseTracks( + release: MusicReleaseModel, + geniusSearchArtist: string, + musicBrainzApi: MusicBrainzAPI, + genius: GeniusClient, + spotify: SpotifyClient, + childOptions: CreateNoteOptions, + useTree: boolean, + songNotesFolder: TFolder | undefined, + ): Promise { + for (const track of release.tracks) { + let lyrics = ''; + let geniusUrl = ''; + if (genius.isConfigured()) { + await new Promise(r => setTimeout(r, 500)); + const hit = await genius.searchFirstSongHit(`${geniusSearchArtist} ${track.title}`); + if (hit) { + geniusUrl = hit.url; + await new Promise(r => setTimeout(r, 600)); + lyrics = await genius.fetchLyricsFromSongPage(hit.url); + } + } + + let spotifyUrl = ''; + if (track.recordingId) { + await new Promise(r => setTimeout(r, 1100)); + try { + spotifyUrl = await musicBrainzApi.fetchSpotifyUrlForRecording(track.recordingId); + } catch (e) { + console.warn(`MDB | Spotify URL for recording ${track.recordingId}:`, e); + } + } + if (!spotifyUrl && spotify.isConfigured()) { + const primaryArtist = release.artists[0] ?? geniusSearchArtist; + console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); + try { + spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); + } catch (e) { + console.warn(`MDB | Spotify search for "${track.title}":`, e); + } + } + + const song = new SongModel({ + type: 'song', + title: track.title, + englishTitle: track.title, + year: release.year, + releaseDate: release.releaseDate, + dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, + url: geniusUrl || release.url, + id: `${release.id}-t${track.number}`, + image: release.image, + subType: 'song', + genres: release.genres ?? [], + artists: release.artists.length > 0 ? release.artists : [geniusSearchArtist], + albumTitle: release.title, + albumReleaseGroupId: release.id, + trackNumber: track.number, + duration: track.duration, + featuredArtists: track.featuredArtists, + geniusUrl, + spotifyUrl, + lyrics, + userData: { personalRating: 0 }, + }); + + const songOpts: CreateNoteOptions = useTree && songNotesFolder ? { ...childOptions, folder: songNotesFolder } : { ...childOptions }; + + await this.createStandardMediaDbNoteFromModel(song, songOpts); + } + } + + private async importMusicReleaseWithOptionalSongs(release: MusicReleaseModel, options: CreateNoteOptions): Promise { try { + const albumNotesFolder = options.folder ?? (await this.mediaTypeManager.getFolder(release, this.app)); + const useTree = this.settings.artistUseFileTreeForSongs; + const importSongs = this.settings.musicReleaseAutomaticallyImportSongs; + + let songNotesFolder: TFolder | undefined; + if (useTree && importSongs) { + const albumSeg = this.safeFileTreeSegment(release.title); + songNotesFolder = await this.ensureVaultFolder(normalizePath(`${albumNotesFolder.path}/${albumSeg}`)); + } + + const albumCreated = await this.createStandardMediaDbNoteFromModel(release, { ...options, folder: albumNotesFolder }); + if (!albumCreated) { + return; + } + + if (!importSongs || release.tracks.length === 0) { + return; + } + + const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; + if (!musicBrainzApi) { + new Notice('MusicBrainz API not available; song notes were skipped.'); + console.warn('MusicBrainz API not available; song notes were skipped.'); + return; + } + const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; const genius = new GeniusClient(geniusToken); if (!genius.isConfigured()) { - new Notice('Artist import: add a Genius API access token in settings to fetch lyrics.'); + new Notice('Album import: Genius token not found! Add a Genius API access token in settings to fetch lyrics.'); + console.warn('Album import: Genius token not found! Add a Genius API access token in settings to fetch lyrics.'); } const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); - const artistApi = this.apiManager.getApiByName('MusicBrainz Artist API') as MusicBrainzArtistAPI | undefined; - const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; - if (!artistApi || !musicBrainzApi) { - new Notice('MusicBrainz APIs not available.'); - return; - } + const geniusSearchArtist = release.artists[0] ?? release.title; + const childOptions: CreateNoteOptions = { + attachTemplate: true, + openNote: false, + attachFile: undefined, + folder: undefined, + }; + + new Notice(`Importing ${release.tracks.length} tracks for ${release.title}…`); + console.log(`Importing ${release.tracks.length} tracks for ${release.title}…`); + + await this.importSongNotesForMusicReleaseTracks( + release, + geniusSearchArtist, + musicBrainzApi, + genius, + spotify, + childOptions, + useTree, + songNotesFolder, + ); + } catch (e) { + console.warn(e); + new Notice(`${e}`); + } + } + private async importArtistDiscography(artist: ArtistModel, options: CreateNoteOptions): Promise { + try { const useTree = this.settings.artistUseFileTreeForSongs; const childOptions: CreateNoteOptions = { attachTemplate: true, @@ -658,7 +798,7 @@ export default class MediaDbPlugin extends Plugin { }; const artistBaseFolder = await this.mediaTypeManager.getFolder(artist, this.app); - const artistNoteFolder = artistBaseFolder; + let artistNoteFolder = artistBaseFolder; let albumNotesFolder = artistBaseFolder; if (useTree) { @@ -672,15 +812,47 @@ export default class MediaDbPlugin extends Plugin { return; } + if (!this.settings.artistAutomaticallyImportReleases) { + new Notice(`✅ Finished artist import for ${artist.title}.`); + console.log(`✅ Finished artist import for ${artist.title}.`); + return; + } + + const geniusToken = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.genius) || undefined; + const genius = new GeniusClient(geniusToken); + if (!genius.isConfigured()) { + new Notice('Artist import: Genius token not found! Add a Genius API access token in settings to fetch lyrics.'); + console.warn('Artist import: Genius token not found! Add a Genius API access token in settings to fetch lyrics.'); + } + + const spotifyClientId = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientId) || undefined; + const spotifyClientSecret = getApiSecretValue(this.app, this.settings.linkedApiSecretIds, ApiSecretID.spotifyClientSecret) || undefined; + const spotify = new SpotifyClient(spotifyClientId, spotifyClientSecret); + + const artistApi = this.apiManager.getApiByName('MusicBrainz Artist API') as MusicBrainzArtistAPI | undefined; + const musicBrainzApi = this.apiManager.getApiByName('MusicBrainz API') as MusicBrainzAPI | undefined; + if (!artistApi || !musicBrainzApi) { + new Notice('MusicBrainz APIs not available.'); + console.warn('MusicBrainz APIs not available.'); + return; + } + let releaseGroupIds: string[]; try { releaseGroupIds = await artistApi.listStudioAlbumReleaseGroupIds(artist.id); } catch (e) { new Notice(`Could not load albums: ${e}`); + console.log(`Could not load albums: ${e}`); return; } - new Notice(`Importing ${releaseGroupIds.length} studio albums and tracks for ${artist.title}…`); + const importSongs = this.settings.musicReleaseAutomaticallyImportSongs; + new Notice( + `Importing ${releaseGroupIds.length} studio albums${importSongs ? ' and tracks' : ''} for ${artist.title}…`, + ); + console.log( + `Importing ${releaseGroupIds.length} studio albums${importSongs ? ' and tracks' : ''} for ${artist.title}…`, + ); for (const rgId of releaseGroupIds) { await new Promise(r => setTimeout(r, 1100)); @@ -694,7 +866,7 @@ export default class MediaDbPlugin extends Plugin { } let songNotesFolder: TFolder | undefined; - if (useTree) { + if (useTree && importSongs) { const albumSeg = this.safeFileTreeSegment(release.title); songNotesFolder = await this.ensureVaultFolder(normalizePath(`${albumNotesFolder.path}/${albumSeg}`)); } @@ -706,69 +878,24 @@ export default class MediaDbPlugin extends Plugin { continue; } - for (const track of release.tracks) { - let lyrics = ''; - let geniusUrl = ''; - if (genius.isConfigured()) { - await new Promise(r => setTimeout(r, 500)); - const hit = await genius.searchFirstSongHit(`${artist.title} ${track.title}`); - if (hit) { - geniusUrl = hit.url; - await new Promise(r => setTimeout(r, 600)); - lyrics = await genius.fetchLyricsFromSongPage(hit.url); - } - } - - let spotifyUrl = ''; - if (track.recordingId) { - await new Promise(r => setTimeout(r, 1100)); - try { - spotifyUrl = await musicBrainzApi.fetchSpotifyUrlForRecording(track.recordingId); - } catch (e) { - console.warn(`MDB | Spotify URL for recording ${track.recordingId}:`, e); - } - } - if (!spotifyUrl && spotify.isConfigured()) { - const primaryArtist = release.artists[0] ?? artist.title; - console.log(`MDB | Spotify API fallback for track "${track.title}" (artist: ${primaryArtist})`); - try { - spotifyUrl = await spotify.searchFirstTrackUrl(track.title, primaryArtist); - } catch (e) { - console.warn(`MDB | Spotify search for "${track.title}":`, e); - } - } - - const song = new SongModel({ - type: 'song', - title: track.title, - englishTitle: track.title, - year: release.year, - releaseDate: release.releaseDate, - dataSource: MUSICBRAINZ_NOTE_DATA_SOURCE, - url: geniusUrl || release.url, - id: `${release.id}-t${track.number}`, - image: release.image, - subType: 'song', - genres: release.genres ?? [], - artists: release.artists.length > 0 ? release.artists : [artist.title], - albumTitle: release.title, - albumReleaseGroupId: release.id, - trackNumber: track.number, - duration: track.duration, - featuredArtists: track.featuredArtists, - geniusUrl, - spotifyUrl, - lyrics, - userData: { personalRating: 0 }, - }); - - const songOpts: CreateNoteOptions = useTree && songNotesFolder ? { ...childOptions, folder: songNotesFolder } : { ...childOptions }; - - await this.createStandardMediaDbNoteFromModel(song, songOpts); + if (!importSongs) { + continue; } + + await this.importSongNotesForMusicReleaseTracks( + release, + artist.title, + musicBrainzApi, + genius, + spotify, + childOptions, + useTree, + songNotesFolder, + ); } - new Notice(`Finished artist import for ${artist.title}.`); + new Notice(`✅ Finished artist import for ${artist.title}.`); + console.log(`✅ Finished artist import for ${artist.title}.`); } catch (e) { console.warn(e); new Notice(`${e}`); @@ -896,6 +1023,60 @@ export default class MediaDbPlugin extends Plugin { dataSource: mediaTypeModel.dataSource, }; } + meta = this.withMovieCurrencyObjectFormat(meta, mediaTypeModel); + meta = this.withSanitizedColonStrings(meta); + return this.withNormalizedTitleAliasMetadata(meta, mediaTypeModel.title); + } + + /** Sanitize missing spaces after colons to avoid Obsidian treating them as URIs */ + private withSanitizedColonStrings(meta: Record): Record { + const next = { ...meta }; + for (const key of Object.keys(next)) { + const val = next[key]; + if (typeof val === 'string') { + // Don't format URLs or similar links + if (val.startsWith('http://') || val.startsWith('https://')) continue; + next[key] = val.replace(/:(?=[^\s\/\\])/g, ': '); + } + } + return next; + } + + /** When enabled, movie budget/revenue become `{ value, currency }` for YAML front matter. */ + private withMovieCurrencyObjectFormat(meta: Record, mediaTypeModel: MediaTypeModel): Record { + if (!this.settings.useObjectFormatForCurrencyValues || mediaTypeModel.getMediaType() !== MediaType.Movie) { + return meta; + } + const next = { ...meta }; + for (const key of ['budget', 'revenue'] as const) { + const raw = next[key]; + if (typeof raw !== 'string') { + continue; + } + const amount = parseUsdWholeDollarsFromDisplayString(raw); + next[key] = amount !== null ? { value: amount, currency: 'USD' } : null; + } + return next; + } + + private withNormalizedTitleAliasMetadata(meta: Record, title: string): Record { + if (!this.settings.addNormalizeTitlesAsAlias) { + return meta; + } + const alias = normalizeTitleForAsciiAlias(title); + if (alias === null) { + return meta; + } + const prev = meta['aliases']; + if (Array.isArray(prev)) { + if (!prev.includes(alias)) { + meta['aliases'] = [...prev, alias]; + } + } else if (typeof prev === 'string') { + meta['aliases'] = prev === alias ? [prev] : [prev, alias]; + } else { + meta['aliases'] = [alias]; + } return meta; } @@ -950,17 +1131,16 @@ export default class MediaDbPlugin extends Plugin { } } - // --- Auto-Tag Logic --- - const tagProps = this.settings.autoTagProperties - .split(',') - .map(s => s.trim().toLowerCase()) - .filter(s => s !== ''); - if (this.settings.enableAutoTagging && tagProps.length > 0) { + // --- Per-Property Auto-Tag Logic --- + const autoTagEntries = this.modelPropertyMapper.getAutoTagKeys(mediaTypeModel.type); + if (autoTagEntries.length > 0) { const existingTags: string[] = Array.isArray(fileMetadata.tags) ? (fileMetadata.tags as string[]) : []; const newTags = new Set(existingTags.filter(t => typeof t === 'string' && t.trim() !== '')); for (const [key, value] of Object.entries(fileMetadata)) { - if (tagProps.includes(key.toLowerCase()) && value) { + const entry = autoTagEntries.find(e => e.key.toLowerCase() === key.toLowerCase()); + if (entry && value) { + const prefix = entry.prefix.trim().replace(/\/$/, ''); // strip trailing slash const valuesToTag = Array.isArray(value) ? value : [value]; for (let v of valuesToTag) { if (typeof v === 'string') { @@ -971,10 +1151,10 @@ export default class MediaDbPlugin extends Plugin { const sanitized = v .trim() .replace(/\s+/g, '-') - .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') + .replace(/[^\wığüşöçIĞÜŞÖÇ/-]/g, '') .toLowerCase(); - if (sanitized) newTags.add(sanitized); + if (sanitized) newTags.add(prefix ? `${prefix}/${sanitized}` : sanitized); } } } @@ -992,11 +1172,17 @@ export default class MediaDbPlugin extends Plugin { } } - // Ensure 'tags' always appears last in frontmatter - if ('tags' in fileMetadata) { - const tagsValue = fileMetadata['tags']; - delete fileMetadata['tags']; - fileMetadata['tags'] = tagsValue; + // Ensure 'pinBottom' properties (including 'tags' if pinned) appear at the absolute bottom + // This guarantees they are listed chronologically below template properties. + const pinnedKeys = this.modelPropertyMapper.getPinnedBottomKeys(mediaTypeModel.type); + for (const key of pinnedKeys) { + if (key in fileMetadata) { + const val = fileMetadata[key]; + delete fileMetadata[key]; + if (val !== null && val !== undefined) { + fileMetadata[key] = val; + } + } } if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { @@ -1010,19 +1196,17 @@ export default class MediaDbPlugin extends Plugin { return fileContent; } - extractManualTags(metadata: Record, autoTagKeys: string): string[] { + extractManualTags(metadata: Record, autoTagEntries: { key: string; prefix: string }[]): string[] { const allTagsRaw = metadata.tags; const allTags = Array.isArray(allTagsRaw) ? allTagsRaw : typeof allTagsRaw === 'string' ? [allTagsRaw] : []; if (allTags.length === 0) return []; - const tagProps = autoTagKeys - .split(',') - .map(s => s.trim().toLowerCase()) - .filter(s => s); const autoTagValues = new Set(); for (const [key, value] of Object.entries(metadata)) { - if (tagProps.includes(key.toLowerCase()) && value) { + const entry = autoTagEntries.find(e => e.key.toLowerCase() === key.toLowerCase()); + if (entry && value) { + const prefix = entry.prefix.trim().replace(/\/$/, ''); const valuesToTag = Array.isArray(value) ? value : [value]; for (const v of valuesToTag) { if (typeof v === 'string') { @@ -1031,9 +1215,9 @@ export default class MediaDbPlugin extends Plugin { const sanitized = clean .trim() .replace(/\s+/g, '-') - .replace(/[^\wığüşöçIĞÜŞÖÇ-]/g, '') + .replace(/[^\wığüşöçIĞÜŞÖÇ/-]/g, '') .toLowerCase(); - if (sanitized) autoTagValues.add(sanitized); + if (sanitized) autoTagValues.add(prefix ? `${prefix}/${sanitized}` : sanitized); } } } @@ -1056,7 +1240,9 @@ export default class MediaDbPlugin extends Plugin { if (typeof arr === 'string' && arr.trim()) return [arr]; return []; }; - const oldManualTags = this.extractManualTags(attachFileMetadata, this.settings.autoTagProperties); + const mediaType = attachFileMetadata.type ?? fileMetadata.type; + const autoTagEntries = this.modelPropertyMapper.getAutoTagKeys(mediaType); + const oldManualTags = this.extractManualTags(attachFileMetadata, autoTagEntries); const oldAliases = rescueArray('aliases'); // TODO: better object merging @@ -1141,6 +1327,16 @@ export default class MediaDbPlugin extends Plugin { * @param fileContent * @param options */ + getResolvedImportPath(mediaTypeModel: MediaTypeModel): string { + let folderPath = this.mediaTypeManager.mediaFolderMap.get(mediaTypeModel.getMediaType()) ?? '/'; + folderPath = this.mediaTypeManager.expandFolderPathForModel(folderPath, mediaTypeModel); + let fileName = this.mediaTypeManager.getFileName(mediaTypeModel); + fileName = replaceIllegalFileNameCharactersInString(fileName); + const dir = folderPath.replace(/^\/+|\/+$/g, ''); + const relative = dir.length > 0 ? `${dir}/${fileName}.md` : `${fileName}.md`; + return normalizePath(relative); + } + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { // find and possibly create the folder set in settings or passed in folder const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); @@ -1155,14 +1351,22 @@ export default class MediaDbPlugin extends Plugin { // look if file already exists and ask if it should be overwritten const file = this.app.vault.getAbstractFileByPath(filePath); if (file) { - let shouldOverwrite = options.overwrite; - if (!shouldOverwrite) { - shouldOverwrite = await new Promise(resolve => { + let choice = options.overwrite ? ConfirmOverwriteChoice.Overwrite : null; + if (!choice) { + choice = await new Promise(resolve => { new ConfirmOverwriteModal(this.app, fileName, resolve).open(); }); } - if (!shouldOverwrite) { + if (choice !== ConfirmOverwriteChoice.Overwrite) { + // To keep old Promise compatibility, return the existing file if kept, or throw + if (choice === ConfirmOverwriteChoice.KeepExisting && file instanceof TFile) { + if (options.openNote) { + const activeLeaf = this.app.workspace.getUnpinnedLeaf(); + if (activeLeaf) await activeLeaf.openFile(file, { state: { mode: 'source' } }); + } + return file; + } throw new Error('MDB | file creation cancelled by user'); } diff --git a/src/modals/BulkRecreateConfirmModal.ts b/src/modals/BulkRecreateConfirmModal.ts new file mode 100644 index 00000000..104542b2 --- /dev/null +++ b/src/modals/BulkRecreateConfirmModal.ts @@ -0,0 +1,71 @@ +import type { App } from 'obsidian'; +import { Modal, Setting } from 'obsidian'; + +export type BulkRecreateMode = 'reorder' | 'full'; + +const MODE_DESCRIPTIONS: Record = { + reorder: + 'All existing note data and custom template values are preserved. Only the property order and Pin settings from your current Property Mapping configuration are re-applied.', + full: '⚠️ Each note is completely rebuilt from scratch using your template. Any changes you made to the note after it was created (e.g. custom fields added by your template) will be reset to their template defaults.', +}; + +export class BulkRecreateConfirmModal extends Modal { + onSubmit: (mode: BulkRecreateMode, silent: boolean) => void; + mode: BulkRecreateMode = 'reorder'; + silentUpdate: boolean = false; + + private descEl!: HTMLParagraphElement; + + constructor(app: App, onSubmit: (mode: BulkRecreateMode, silent: boolean) => void) { + super(app); + this.onSubmit = onSubmit; + } + + onOpen() { + const { contentEl } = this; + contentEl.createEl('h2', { text: 'Bulk Recreate Notes' }); + contentEl.createEl('p', { + text: 'You are about to process all Media DB notes in this folder. Choose how each note should be rebuilt:', + cls: 'mod-muted', + }); + + this.descEl = contentEl.createEl('p', { + text: MODE_DESCRIPTIONS[this.mode], + }); + this.descEl.style.cssText = 'padding: 8px 12px; border-left: 3px solid var(--interactive-accent); margin-bottom: 12px; font-size: var(--font-ui-small);'; + + new Setting(contentEl) + .setName('Recreate Mode') + .addDropdown(drop => + drop + .addOption('reorder', 'Apply Property Order (Safe)') + .addOption('full', 'Full Template Reset') + .setValue(this.mode) + .onChange(value => { + this.mode = value as BulkRecreateMode; + this.descEl.setText(MODE_DESCRIPTIONS[this.mode]); + this.descEl.style.borderLeftColor = this.mode === 'full' ? 'var(--color-red)' : 'var(--interactive-accent)'; + }), + ); + + new Setting(contentEl) + .setName('Update Silently (No Confirmations)') + .setDesc('If enabled, all updates will run without asking for individual confirmation for each file.') + .addToggle(toggle => toggle.setValue(this.silentUpdate).onChange(value => (this.silentUpdate = value))); + + new Setting(contentEl).addButton(btn => + btn + .setButtonText('Start Recreate') + .setWarning() + .onClick(() => { + this.close(); + this.onSubmit(this.mode, this.silentUpdate); + }), + ); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/modals/BulkUpdateConfirmModal.ts b/src/modals/BulkUpdateConfirmModal.ts index 5b9effe4..7afb18e8 100644 --- a/src/modals/BulkUpdateConfirmModal.ts +++ b/src/modals/BulkUpdateConfirmModal.ts @@ -1,8 +1,9 @@ -import { type App, Modal, Setting } from 'obsidian'; +import type {App} from 'obsidian'; +import { Modal, Setting } from 'obsidian'; export class BulkUpdateConfirmModal extends Modal { onSubmit: (silent: boolean) => void; - silentUpdate: boolean = true; + silentUpdate: boolean = false; constructor(app: App, onSubmit: (silent: boolean) => void) { super(app); @@ -17,19 +18,17 @@ export class BulkUpdateConfirmModal extends Modal { new Setting(contentEl) .setName('Update Silently (No Confirmations)') .setDesc('If enabled, all updates will aggressively overwrite the note frontmatter without asking for individual confirmation for each file.') - .addToggle(toggle => toggle - .setValue(this.silentUpdate) - .onChange(value => (this.silentUpdate = value)) - ); + .addToggle(toggle => toggle.setValue(this.silentUpdate).onChange(value => (this.silentUpdate = value))); - new Setting(contentEl) - .addButton(btn => btn + new Setting(contentEl).addButton(btn => + btn .setButtonText('Start Update') .setCta() .onClick(() => { this.close(); this.onSubmit(this.silentUpdate); - })); + }), + ); } onClose() { diff --git a/src/modals/CompletionModal.ts b/src/modals/CompletionModal.ts index 233c3008..10b1d958 100644 --- a/src/modals/CompletionModal.ts +++ b/src/modals/CompletionModal.ts @@ -1,4 +1,5 @@ -import { type App, Modal, ButtonComponent } from 'obsidian'; +import type {App} from 'obsidian'; +import { Modal, ButtonComponent } from 'obsidian'; export interface CompletionResult { /** Title shown in the modal header */ diff --git a/src/modals/ConfirmOverwriteModal.ts b/src/modals/ConfirmOverwriteModal.ts index 9852da26..48d0cff7 100644 --- a/src/modals/ConfirmOverwriteModal.ts +++ b/src/modals/ConfirmOverwriteModal.ts @@ -1,45 +1,125 @@ import type { App } from 'obsidian'; import { Modal, Setting } from 'obsidian'; +export enum ConfirmOverwriteChoice { + Overwrite = 'overwrite', + Skip = 'skip', + Abort = 'abort', + KeepExisting = 'keepExisting', +} + export class ConfirmOverwriteModal extends Modal { - result: boolean = false; - onSubmit: (result: boolean) => void; + choice: ConfirmOverwriteChoice = ConfirmOverwriteChoice.Skip; + onSubmit: (choice: ConfirmOverwriteChoice) => void; fileName: string; + private readonly showAbortRemaining: boolean; + private readonly showSkip: boolean; + /** + * When set: body text plus Abort (red) / optional Skip / No / Yes. + * When unset and showAbortRemaining: legacy Skip / warning Abort / Yes. + */ + private readonly detail: string | undefined; - constructor(app: App, fileName: string, onSubmit: (result: boolean) => void) { + constructor( + app: App, + fileName: string, + onSubmit: (choice: ConfirmOverwriteChoice) => void, + opts?: { + showAbortRemaining?: boolean; + showSkip?: boolean; + /** Explains overwrite vs keep vs abort for chained imports (artist discography, release + tracks). */ + detail?: string; + }, + ) { super(app); this.fileName = fileName; this.onSubmit = onSubmit; + this.showAbortRemaining = opts?.showAbortRemaining ?? false; + this.showSkip = opts?.showSkip ?? false; + this.detail = opts?.detail; } onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: 'File already exists' }); - contentEl.createEl('p', { text: `The file "${this.fileName}" already exists. Do you want to overwrite it?` }); + + const defaultParagraph = `The file "${this.fileName}" already exists. Do you want to overwrite it?`; + contentEl.createEl('p', { text: this.detail ?? defaultParagraph }); contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); const bottomSettingRow = new Setting(contentEl); - - bottomSettingRow.addButton(btn => { - btn.setButtonText('No'); - btn.onClick(() => this.close()); - btn.buttonEl.addClass('media-db-plugin-button'); - }); - bottomSettingRow.addButton(btn => { - btn.setButtonText('Yes'); - btn.setCta(); - btn.onClick(() => { - this.result = true; - this.close(); + if (this.detail !== undefined) { + if (this.showAbortRemaining) { + bottomSettingRow.addButton(btn => { + btn.setButtonText('Abort'); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.Abort; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + btn.buttonEl.addClass('media-db-plugin-abort-button'); + }); + } + if (this.showSkip) { + bottomSettingRow.addButton(btn => { + btn.setButtonText('Skip'); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.Skip; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + } + bottomSettingRow.addButton(btn => { + btn.setButtonText('No'); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.KeepExisting; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Yes'); + btn.setCta(); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.Overwrite; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + } else { + if (this.showAbortRemaining) { + bottomSettingRow.addButton(btn => { + btn.setButtonText('Abort'); + btn.setWarning(); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.Abort; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + } + bottomSettingRow.addButton(btn => { + btn.setButtonText(this.showAbortRemaining ? 'Skip' : 'No'); + btn.onClick(() => this.close()); + btn.buttonEl.addClass('media-db-plugin-button'); + }); + bottomSettingRow.addButton(btn => { + btn.setButtonText('Yes'); + btn.setCta(); + btn.onClick(() => { + this.choice = ConfirmOverwriteChoice.Overwrite; + this.close(); + }); + btn.buttonEl.addClass('media-db-plugin-button'); }); - btn.buttonEl.addClass('media-db-plugin-button'); - }); + } } onClose(): void { const { contentEl } = this; contentEl.empty(); - this.onSubmit(this.result); + this.onSubmit(this.choice); } } diff --git a/src/modals/MediaDbPreviewModal.ts b/src/modals/MediaDbPreviewModal.ts index 931556c1..3880f964 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/src/modals/MediaDbPreviewModal.ts @@ -57,6 +57,10 @@ export class MediaDbPreviewModal extends Modal { } catch (e) { console.warn(`mdb | error during rendering of preview`, e); } + + const importPath = this.plugin.getResolvedImportPath(result); + const pathRow = previewWrapper.createDiv({ cls: 'media-db-plugin-preview-import-path' }); + pathRow.createEl('code', { text: importPath }); } contentEl.createDiv({ cls: 'media-db-plugin-spacer' }); diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 786df40a..39821aba 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -39,11 +39,90 @@ export class MediaDbSearchResultModal extends SelectModal { this.skipCallback = skipCallback; } + // Different rate limit delay based on API source, MAL APIs = max 3 per second so 400ms between requests to be safe + private getDelayForApi(dataSource: string): number { + const isMalApi = dataSource === 'MALAPI' || dataSource === 'MALAPIManga'; + return isMalApi ? 400 : 200; + } + // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); - el.createEl('small', { text: `${item.getSummary()}\n` }); - el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); + el.addClass('media-db-plugin-select-element-flex'); + el.style.display = 'flex'; + el.style.gap = '8px'; + el.style.alignItems = 'flex-start'; + + const thumb = el.createDiv({ cls: 'media-db-plugin-select-thumb' }); + + let imgEl: HTMLImageElement | undefined; + + const setImage = (url: string) => { + if (!imgEl) { + imgEl = document.createElement('img'); + imgEl.loading = 'lazy'; + imgEl.alt = item.title; + thumb.empty(); + thumb.appendChild(imgEl); + imgEl.style.width = '100%'; + imgEl.style.height = '100%'; + imgEl.style.objectFit = 'cover'; + imgEl.onerror = () => { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + }; + } + imgEl.src = url; + }; + + const content = el.createDiv({ cls: 'media-db-plugin-select-content' }); + + const titleEl = content.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item), cls: 'media-db-plugin-select-title' }); + const summaryEl = content.createEl('small', { text: `${item.getSummary()}\n` }); + content.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); + + const updateSummary = () => { + titleEl.textContent = this.plugin.mediaTypeManager.getFileName(item); + summaryEl.textContent = `${item.getSummary()}\n`; + }; + + if (item.image && item.image !== 'NSFW') { + if (String(item.image).includes('null')) { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + } else { + setImage(item.image); + } + } else if (item.image === 'NSFW') { + thumb.createEl('span', { text: 'NSFW' }); + } else { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + + const needsFetch = !item.image || !item.year; + if (needsFetch) { + const apiDelay = this.getDelayForApi(item.dataSource); + const delayMs = (parseInt(el.id.split('-').pop() ?? '0') ?? 0) * apiDelay; + setTimeout(async () => { + if (item.image && item.year) return; + try { + const detailed = await this.plugin.apiManager.queryDetailedInfo(item); + if (detailed?.image && !item.image) { + item.image = detailed.image; + setImage(detailed.image); + } + if (!item.year && detailed?.year) { + item.year = detailed.year; + updateSummary(); + } + } catch (e) { + console.warn('MDB | Failed to fetch detail', e); + } + }, delayMs); + } + } } // Perform action on the selected suggestion. diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index 0066c31e..d7519a1d 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -24,9 +24,45 @@ export class MediaDbSeasonSelectModal extends SelectModal { + thumb.empty(); + const placeholderSpan = thumb.createEl('span', { text: '📷' }); + placeholderSpan.style.fontSize = '24px'; + }; + thumb.appendChild(img); + } else { + thumb.createEl('span', { text: '📷' }).style.fontSize = '24px'; + } + + const content = el.createDiv(); + content.style.flex = '1'; + content.style.minWidth = '0'; + content.createEl('div', { text: `${season.name}` }); if (season.air_date) { - el.createEl('small', { text: `Air date: ${season.air_date}` }); + content.createEl('small', { text: `Air date: ${season.air_date}` }); } } diff --git a/src/modals/PropertyMappingModal.ts b/src/modals/PropertyMappingModal.ts index 8f5adccd..388abbb3 100644 --- a/src/modals/PropertyMappingModal.ts +++ b/src/modals/PropertyMappingModal.ts @@ -2,10 +2,10 @@ import type { App } from 'obsidian'; import { Modal } from 'obsidian'; import { render } from 'solid-js/web'; import type MediaDbPlugin from '../main'; -import type { MediaType } from '../utils/MediaType'; -import { mediaTypeDisplayName } from '../utils/Utils'; import type { PropertyMappingModelData } from '../settings/PropertyMapping'; import PropertyMappingModelComponent from '../settings/PropertyMappingModelComponent'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; export class PropertyMappingModal extends Modal { private disposeSolid?: () => void; diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index e7e100b1..c72da64c 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -35,9 +35,27 @@ export abstract class MediaTypeModel { abstract getTags(): string[]; toMetaDataObject(): Record { - const obj: Record = { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; + const base = { ...this.getWithOutUserData() }; + + // Extract description-like fields to pin them just before tags + const hasSummary = Object.hasOwn(base, 'summary'); + const hasPlot = Object.hasOwn(base, 'plot'); + const summary = base.summary; + const plot = base.plot; + delete base.summary; + delete base.plot; + + const obj: Record = { ...base, ...this.userData }; + // year: 0 means "unknown" — write null so YAML shows blank (None) instead of 0 - if (obj['year'] === 0) obj['year'] = null; + if (obj.year === 0) obj.year = null; + + // Pin summary / plot just above tags — always include them if the model has the field + // (empty string → null so YAML renders as blank rather than empty quotes) + if (hasSummary) obj.summary = summary || null; + if (hasPlot) obj.plot = plot || null; + + obj.tags = this.getTags().join('/'); return obj; } diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 1ff0c809..b87c0325 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -1,5 +1,9 @@ import type MediaDbPlugin from '../main'; +import { ArtistModel } from '../models/ArtistModel'; +import { MusicReleaseModel } from '../models/MusicReleaseModel'; +import { MediaType } from '../utils/MediaType'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; +import { coerceYear } from '../utils/Utils'; import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { @@ -35,71 +39,70 @@ export class PropertyMapper { const propertyMappings = propertyMappingModel.properties; const newObj: Record = {}; + const handledKeys = new Set(); - const entityProps = this.plugin.settings.autoTagEntities.split(',').map(s => s.trim().toLowerCase()).filter(s => s); - - // 1. Preprocess global wiki-links on the raw object first - if (this.plugin.settings.enableWikiLinkParsing && entityProps.length > 0) { - for (const [key, value] of Object.entries(obj)) { - if (key === 'aliases') continue; - if (entityProps.includes(key.toLowerCase())) { - const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; - const formatWiki = (v: unknown) => { - if (typeof v !== 'string') return v; - let clean = v.replace(/^\[\[(.*?)\]\]$/, '$1'); - if (clean.includes('|')) clean = clean.split('|')[1]; - return `[[${folderPrefix}${clean}|${clean}]]`; - }; + // 1. Process keys exactly in the order of the user's Property Mappings array + for (const propertyMapping of propertyMappings) { + const key = propertyMapping.property; + if (key === 'aliases') { + handledKeys.add(key); + continue; + } + + if (Object.hasOwn(obj, key)) { + const value = obj[key]; + handledKeys.add(key); + + let finalValue = value; + if (propertyMapping.wikilink) { + const useArtistFileNameForArtists = + propertyMapping.property === 'artists' && + (internalMediaType === MediaType.Song || internalMediaType === MediaType.MusicRelease); + const useMusicReleaseFileNameForAlbumTitle = + propertyMapping.property === 'albumTitle' && internalMediaType === MediaType.Song; if (typeof value === 'string') { - obj[key] = formatWiki(value); + if (useArtistFileNameForArtists) { + finalValue = this.artistTitleWikilink(value); + } else if (useMusicReleaseFileNameForAlbumTitle) { + finalValue = this.songAlbumTitleWikilink(value, obj); + } else { + finalValue = `[[${value}]]`; + } } else if (Array.isArray(value)) { - obj[key] = value.map(formatWiki); + finalValue = value.map((v: unknown) => { + if (typeof v !== 'string') { + return v; + } + if (useArtistFileNameForArtists) { + return this.artistTitleWikilink(v); + } + if (useMusicReleaseFileNameForAlbumTitle) { + return this.songAlbumTitleWikilink(v, obj); + } + return `[[${v}]]`; + }); } } + + if (propertyMapping.mapping === PropertyMappingOption.Map) { + newObj[propertyMapping.newProperty] = finalValue; + } else if (propertyMapping.mapping === PropertyMappingOption.Remove) { + // do nothing + } else if (propertyMapping.mapping === PropertyMappingOption.Default) { + newObj[key] = finalValue; + } } } - // 2. Map standard properties + // 2. Append any remaining unmatched keys from obj (to preserve unhandled data) for (const [key, value] of Object.entries(obj)) { - if (key === 'aliases') { - continue; - } - for (const propertyMapping of propertyMappings) { - if (propertyMapping.property === key) { - let finalValue = value; - if (propertyMapping.wikilink) { - const folderPrefix = this.plugin.settings.wikiFolder ? `${this.plugin.settings.wikiFolder}/` : ''; - // Resolve the originating API so it can provide property-specific link formatting - const api = typeof obj.dataSource === 'string' - ? this.plugin.apiManager.getApiByName(obj.dataSource) - : undefined; - const wikilink = (v: unknown): unknown => { - if (typeof v !== 'string') return v; - if (api) return api.wikilinkValueFor(key, v, obj, folderPrefix); - const clean = v.replace(/^\[\[(.*?)\]\]$/, '$1').split('|').pop()!; - return `[[${folderPrefix}${clean}|${clean}]]`; - }; - if (typeof value === 'string') { - finalValue = wikilink(value); - } else if (Array.isArray(value)) { - finalValue = value.map(wikilink); - } - } - if (propertyMapping.mapping === PropertyMappingOption.Map) { - // @ts-ignore - newObj[propertyMapping.newProperty] = finalValue; - } else if (propertyMapping.mapping === PropertyMappingOption.Remove) { - // do nothing - } else if (propertyMapping.mapping === PropertyMappingOption.Default) { - // @ts-ignore - newObj[key] = finalValue; - } - break; - } + if (!handledKeys.has(key) && key !== 'aliases') { + newObj[key] = value; } } + // 3. Handle aliases if (Object.hasOwn(obj, 'aliases')) { const aliasesPm = propertyMappings.find(p => p.property === 'aliases'); if (aliasesPm?.mapping !== PropertyMappingOption.Remove) { @@ -118,6 +121,44 @@ export class PropertyMapper { return newObj; } + getPinnedBottomKeys(type: unknown): string[] { + const internalMediaType = resolveMetadataTypeToMediaType(this.plugin.settings, type); + if (!internalMediaType) return []; + + const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === internalMediaType); + if (!propertyMappingModel) return []; + + const pinnedKeys: string[] = []; + for (const mapping of propertyMappingModel.properties) { + if (mapping.pinBottom) { + // The key to pin is the NEW key if mapped + if (mapping.mapping === PropertyMappingOption.Map) { + pinnedKeys.push(mapping.newProperty); + } else if (mapping.mapping === PropertyMappingOption.Default) { + pinnedKeys.push(mapping.property); + } + } + } + return pinnedKeys; + } + + getAutoTagKeys(type: unknown): { key: string; prefix: string }[] { + const internalMediaType = resolveMetadataTypeToMediaType(this.plugin.settings, type); + if (!internalMediaType) return []; + + const propertyMappingModel = this.plugin.settings.propertyMappingModels.find(x => x.type === internalMediaType); + if (!propertyMappingModel) return []; + + const autoTagKeys: { key: string; prefix: string }[] = []; + for (const mapping of propertyMappingModel.properties) { + if (mapping.autoTag && mapping.mapping !== PropertyMappingOption.Remove) { + const key = mapping.mapping === PropertyMappingOption.Map ? mapping.newProperty : mapping.property; + autoTagKeys.push({ key, prefix: mapping.autoTagPrefix ?? '' }); + } + } + return autoTagKeys; + } + private static mergeAliasValues(existing: unknown, added: unknown): string[] { const toStrings = (v: unknown): string[] => { if (v == null) { @@ -206,4 +247,68 @@ export class PropertyMapper { return originalObj; } + /** + * Wikilink for an artist name using the Artist file name template as the link target and the raw artist title as the display alias. + */ + private artistTitleWikilink(artistTitle: string): string { + const title = artistTitle.trim(); + const artistModel = new ArtistModel({ + type: 'artist', + title, + englishTitle: title, + year: 0, + beginYear: '', + releaseDate: '', + dataSource: '', + url: '', + id: '', + country: '', + disambiguation: '', + isni: '', + genres: [], + image: '', + officialWebsite: '', + subType: 'artist', + userData: { personalRating: 0 }, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(artistModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; + } + + /** + * Wikilink for a song's release title using the Music Release file name template; fills artists/year from the song metadata when present. + */ + private songAlbumTitleWikilink(albumTitle: string, songMeta: Record): string { + const title = albumTitle.trim(); + const artistsRaw = songMeta.artists; + const artists = Array.isArray(artistsRaw) + ? artistsRaw.filter((a): a is string => typeof a === 'string') + : []; + const year = coerceYear(songMeta.year); + const releaseModel = new MusicReleaseModel({ + type: 'musicRelease', + title, + englishTitle: title, + year, + releaseDate: '', + dataSource: '', + url: '', + id: '', + image: '', + artists, + genres: [], + subType: 'album', + language: '', + rating: 0, + userData: { personalRating: 0 }, + }); + const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; + } } diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index 649320e9..30fed3b9 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -1,277 +1,297 @@ -import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; -import type { MediaType } from '../utils/MediaType'; -import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; - -// Plain object interfaces for serialization -export interface PropertyMappingData { - property: string; - newProperty: string; - mapping: PropertyMappingOption; - locked?: boolean; - wikilink?: boolean; -} - -export interface PropertyMappingModelData { - type: MediaType; - properties: PropertyMappingData[]; -} - -export enum PropertyMappingOption { - Default = 'default', - Map = 'remap', - Remove = 'remove', -} - -export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; - -const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id'] as const; - -export class PropertyMappingModel { - type: MediaType; - properties: PropertyMapping[]; - - constructor(type: MediaType, properties?: PropertyMapping[]) { - this.type = type; - this.properties = properties ?? []; - } - - validate(): { res: boolean; err?: Error } { - console.debug(`MDB | validated property mappings for ${this.type}`); - - // check properties - for (const property of this.properties) { - const propertyValidation = property.validate(); - if (!propertyValidation.res) { - return { - res: false, - err: propertyValidation.err, - }; - } - } - - // check for name collisions - for (const property of this.getMappedProperties()) { - const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty); - if (propertiesWithSameTarget.length === 0) { - // if we get there, then something in this code is wrong - } else if (propertiesWithSameTarget.length === 1) { - // all good - } else { - // two or more properties are mapped to the same property - return { - res: false, - err: new PropertyMappingNameConflictError( - `Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`, - ), - }; - } - } - // remapped properties may not have the same name as any original property - for (const property of this.getMappedProperties()) { - const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property); - if (propertiesWithSameTarget.length === 0) { - // all good - } else { - // a mapped property shares the same name with an original property - return { - res: false, - err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`), - }; - } - } - - const dataSourceRule = this.properties.find(p => p.property === 'dataSource'); - if (dataSourceRule?.mapping === PropertyMappingOption.Remove && !musicBrainzRegisteredApiName(this.type)) { - return { - res: false, - err: new PropertyMappingValidationError( - `Removing dataSource is only allowed for artist, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, - ), - }; - } - - return { - res: true, - }; - } - - getMappedProperties(): PropertyMapping[] { - return this.properties.filter(x => x.mapping === PropertyMappingOption.Map); - } - - copy(): PropertyMappingModel { - const copy = new PropertyMappingModel(this.type); - for (const property of this.properties) { - const propertyCopy = new PropertyMapping(property.property, property.newProperty, property.mapping, property.locked, property.wikilink); - copy.properties.push(propertyCopy); - } - return copy; - } - - // Serialization - returns a plain object that can be JSON.stringify'd - toJSON(): PropertyMappingModelData { - return { - type: this.type, - properties: this.properties.map(p => p.toJSON()), - }; - } - - // Deserialization - creates a PropertyMappingModel from a plain object - static fromJSON(json: PropertyMappingModelData): PropertyMappingModel { - return new PropertyMappingModel( - json.type, - json.properties.map(p => PropertyMapping.fromJSON(p)), - ); - } - - /** - * Migrates loaded settings to match the structure of default settings. - * - Adds new properties from defaults that don't exist in loaded settings - * - Preserves user customizations from loaded settings - * - Updates locked status from defaults - * - * @param loadedModels - Models loaded from disk (may be outdated) - * @param defaultModels - Current default models (source of truth for structure) - * @returns Migrated models with correct structure and preserved user settings - */ - static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { - const migratedModels: PropertyMappingModel[] = []; - - for (const defaultModel of defaultModels) { - const loadedModel = loadedModels.find(m => m.type === defaultModel.type); - - if (!loadedModel) { - // New model type - use default - migratedModels.push(defaultModel); - continue; - } - - // Migrate properties - const migratedProperties: PropertyMapping[] = []; - for (const defaultProperty of defaultModel.properties) { - const loadedProperty = loadedModel.properties.find(p => p.property === defaultProperty.property); - - if (!loadedProperty) { - // New property - use default - migratedProperties.push(defaultProperty); - } else { - // Existing property - merge: take locked from default, customizations from loaded - migratedProperties.push( - new PropertyMapping( - loadedProperty.property, - loadedProperty.newProperty, - loadedProperty.mapping, - defaultProperty.locked, // locked status from default - loadedProperty.wikilink ?? false, - ), - ); - } - } - - migratedModels.push(new PropertyMappingModel(defaultModel.type, migratedProperties)); - } - - return migratedModels; - } -} - -export class PropertyMapping { - property: string; - newProperty: string; - locked: boolean; - mapping: PropertyMappingOption; - wikilink: boolean; - - constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean) { - this.property = property; - this.newProperty = newProperty; - this.mapping = mapping; - this.locked = locked ?? false; - this.wikilink = wikilink ?? false; - } - - validate(): { res: boolean; err?: Error } { - // locked property may only be default - if (this.locked) { - if (this.mapping === PropertyMappingOption.Remove) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`), - }; - } - if (this.mapping === PropertyMappingOption.Map) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`), - }; - } - } - - if ( - (METADATA_KEYS_REQUIRED_IN_NOTE as readonly string[]).includes(this.property) && - this.mapping === PropertyMappingOption.Remove - ) { - return { - res: false, - err: new PropertyMappingValidationError( - `Error in property mapping "${this.toString()}": type and id must appear in the note (you can remap them, but not remove them).`, - ), - }; - } - - if (this.mapping === PropertyMappingOption.Default) { - return { res: true }; - } - if (this.mapping === PropertyMappingOption.Remove) { - return { res: true }; - } - - if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) { - return { - res: false, - err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and may only contain letters and underscores.`), - }; - } - - if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) { - return { - res: false, - err: new PropertyMappingValidationError( - `Error in property mapping "${this.toString()}": new property may not be empty and may only contain letters and underscores.`, - ), - }; - } - - return { - res: true, - }; - } - - toString(): string { - if (this.mapping === PropertyMappingOption.Default) { - return this.property; - } else if (this.mapping === PropertyMappingOption.Map) { - return `${this.property} -> ${this.newProperty}`; - } else if (this.mapping === PropertyMappingOption.Remove) { - return `remove ${this.property}`; - } - - return this.property; - } - - // Serialization - returns a plain object - toJSON(): PropertyMappingData { - return { - property: this.property, - newProperty: this.newProperty, - mapping: this.mapping, - locked: this.locked, - wikilink: this.wikilink, - }; - } - - // Deserialization - creates a PropertyMapping from a plain object - static fromJSON(json: PropertyMappingData): PropertyMapping { - return new PropertyMapping(json.property, json.newProperty, json.mapping, json.locked, json.wikilink); - } -} +import { musicBrainzRegisteredApiName } from '../api/musicBrainzConstants'; +import type { MediaType } from '../utils/MediaType'; +import { containsOnlyLettersAndUnderscores, PropertyMappingNameConflictError, PropertyMappingValidationError } from '../utils/Utils'; + +// Plain object interfaces for serialization +export interface PropertyMappingData { + property: string; + newProperty: string; + mapping: PropertyMappingOption; + locked?: boolean; + wikilink?: boolean; + pinBottom?: boolean; + autoTag?: boolean; + autoTagPrefix?: string; +} + +export interface PropertyMappingModelData { + type: MediaType; + properties: PropertyMappingData[]; +} + +export enum PropertyMappingOption { + Default = 'default', + Map = 'remap', + Remove = 'remove', +} + +export const propertyMappingOptions = [PropertyMappingOption.Default, PropertyMappingOption.Map, PropertyMappingOption.Remove]; + +const METADATA_KEYS_REQUIRED_IN_NOTE = ['type', 'id'] as const; + +export class PropertyMappingModel { + type: MediaType; + properties: PropertyMapping[]; + + constructor(type: MediaType, properties?: PropertyMapping[]) { + this.type = type; + this.properties = properties ?? []; + } + + validate(): { res: boolean; err?: Error } { + console.debug(`MDB | validated property mappings for ${this.type}`); + + // check properties + for (const property of this.properties) { + const propertyValidation = property.validate(); + if (!propertyValidation.res) { + return { + res: false, + err: propertyValidation.err, + }; + } + } + + // check for name collisions + for (const property of this.getMappedProperties()) { + const propertiesWithSameTarget = this.getMappedProperties().filter(x => x.newProperty === property.newProperty); + if (propertiesWithSameTarget.length === 0) { + // if we get there, then something in this code is wrong + } else if (propertiesWithSameTarget.length === 1) { + // all good + } else { + // two or more properties are mapped to the same property + return { + res: false, + err: new PropertyMappingNameConflictError( + `Multiple remapped properties (${propertiesWithSameTarget.map(x => x.toString()).toString()}) may not share the same name.`, + ), + }; + } + } + // remapped properties may not have the same name as any original property + for (const property of this.getMappedProperties()) { + const propertiesWithSameTarget = this.properties.filter(x => x.newProperty === property.property); + if (propertiesWithSameTarget.length === 0) { + // all good + } else { + // a mapped property shares the same name with an original property + return { + res: false, + err: new PropertyMappingNameConflictError(`Remapped property (${property}) may not share it's new name with an existing property.`), + }; + } + } + + const dataSourceRule = this.properties.find(p => p.property === 'dataSource'); + if (dataSourceRule?.mapping === PropertyMappingOption.Remove && !musicBrainzRegisteredApiName(this.type)) { + return { + res: false, + err: new PropertyMappingValidationError( + `Removing dataSource is only allowed for artist, music release, and song (MusicBrainz). For "${this.type}" notes, dataSource is required to choose an API.`, + ), + }; + } + + return { + res: true, + }; + } + + getMappedProperties(): PropertyMapping[] { + return this.properties.filter(x => x.mapping === PropertyMappingOption.Map); + } + + copy(): PropertyMappingModel { + const copy = new PropertyMappingModel(this.type); + for (const property of this.properties) { + const propertyCopy = new PropertyMapping( + property.property, + property.newProperty, + property.mapping, + property.locked, + property.wikilink, + property.pinBottom, + property.autoTag, + property.autoTagPrefix, + ); + copy.properties.push(propertyCopy); + } + return copy; + } + + // Serialization - returns a plain object that can be JSON.stringify'd + toJSON(): PropertyMappingModelData { + return { + type: this.type, + properties: this.properties.map(p => p.toJSON()), + }; + } + + // Deserialization - creates a PropertyMappingModel from a plain object + static fromJSON(json: PropertyMappingModelData): PropertyMappingModel { + return new PropertyMappingModel( + json.type, + json.properties.map(p => PropertyMapping.fromJSON(p)), + ); + } + + /** + * Migrates loaded settings to match the structure of default settings. + * - Adds new properties from defaults that don't exist in loaded settings + * - Preserves user customizations from loaded settings + * - Updates locked status from defaults + * + * @param loadedModels - Models loaded from disk (may be outdated) + * @param defaultModels - Current default models (source of truth for structure) + * @returns Migrated models with correct structure and preserved user settings + */ + static migrateModels(loadedModels: PropertyMappingModelData[], defaultModels: PropertyMappingModel[]): PropertyMappingModel[] { + const migratedModels: PropertyMappingModel[] = []; + + for (const defaultModel of defaultModels) { + const loadedModel = loadedModels.find(m => m.type === defaultModel.type); + + if (!loadedModel) { + // New model type - use default + migratedModels.push(defaultModel); + continue; + } + + // Migrate properties + const migratedProperties: PropertyMapping[] = []; + for (const defaultProperty of defaultModel.properties) { + const loadedProperty = loadedModel.properties.find(p => p.property === defaultProperty.property); + + if (!loadedProperty) { + // New property - use default + migratedProperties.push(defaultProperty); + } else { + // Existing property - merge: take locked from default, customizations from loaded + migratedProperties.push( + new PropertyMapping( + loadedProperty.property, + loadedProperty.newProperty, + loadedProperty.mapping, + defaultProperty.locked, + loadedProperty.wikilink ?? false, + loadedProperty.pinBottom ?? false, + loadedProperty.autoTag ?? false, + loadedProperty.autoTagPrefix ?? '', + ), + ); + } + } + + migratedModels.push(new PropertyMappingModel(defaultModel.type, migratedProperties)); + } + + return migratedModels; + } +} + +export class PropertyMapping { + property: string; + newProperty: string; + locked: boolean; + mapping: PropertyMappingOption; + wikilink: boolean; + pinBottom: boolean; + autoTag: boolean; + autoTagPrefix: string; + + constructor(property: string, newProperty: string, mapping: PropertyMappingOption, locked?: boolean, wikilink?: boolean, pinBottom?: boolean, autoTag?: boolean, autoTagPrefix?: string) { + this.property = property; + this.newProperty = newProperty; + this.mapping = mapping; + this.locked = locked ?? false; + this.wikilink = wikilink ?? false; + this.pinBottom = pinBottom ?? false; + this.autoTag = autoTag ?? false; + this.autoTagPrefix = autoTagPrefix ?? ''; + } + + validate(): { res: boolean; err?: Error } { + // locked property may only be default + if (this.locked) { + if (this.mapping === PropertyMappingOption.Remove) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be removed.`), + }; + } + if (this.mapping === PropertyMappingOption.Map) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": locked property may not be remapped.`), + }; + } + } + + if ((METADATA_KEYS_REQUIRED_IN_NOTE as readonly string[]).includes(this.property) && this.mapping === PropertyMappingOption.Remove) { + return { + res: false, + err: new PropertyMappingValidationError( + `Error in property mapping "${this.toString()}": type and id must appear in the note (you can remap them, but not remove them).`, + ), + }; + } + + if (this.mapping === PropertyMappingOption.Default) { + return { res: true }; + } + if (this.mapping === PropertyMappingOption.Remove) { + return { res: true }; + } + + if (!this.property || !containsOnlyLettersAndUnderscores(this.property)) { + return { + res: false, + err: new PropertyMappingValidationError(`Error in property mapping "${this.toString()}": property may not be empty and may only contain letters and underscores.`), + }; + } + + if (!this.newProperty || !containsOnlyLettersAndUnderscores(this.newProperty)) { + return { + res: false, + err: new PropertyMappingValidationError( + `Error in property mapping "${this.toString()}": new property may not be empty and may only contain letters and underscores.`, + ), + }; + } + + return { + res: true, + }; + } + + toString(): string { + if (this.mapping === PropertyMappingOption.Default) { + return this.property; + } else if (this.mapping === PropertyMappingOption.Map) { + return `${this.property} -> ${this.newProperty}`; + } else if (this.mapping === PropertyMappingOption.Remove) { + return `remove ${this.property}`; + } + + return this.property; + } + + // Serialization - returns a plain object + toJSON(): PropertyMappingData { + return { + property: this.property, + newProperty: this.newProperty, + mapping: this.mapping, + locked: this.locked, + wikilink: this.wikilink, + pinBottom: this.pinBottom, + autoTag: this.autoTag, + autoTagPrefix: this.autoTagPrefix, + }; + } + + static fromJSON(json: PropertyMappingData): PropertyMapping { + return new PropertyMapping(json.property, json.newProperty, json.mapping, json.locked, json.wikilink, json.pinBottom, json.autoTag, json.autoTagPrefix); + } +} diff --git a/src/settings/PropertyMappingModelComponent.tsx b/src/settings/PropertyMappingModelComponent.tsx index a533687a..a67f4a31 100644 --- a/src/settings/PropertyMappingModelComponent.tsx +++ b/src/settings/PropertyMappingModelComponent.tsx @@ -1,128 +1,224 @@ -import { createMemo, For, Show } from 'solid-js'; -import { createStore } from 'solid-js/store'; -import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; -import type { MediaType } from '../utils/MediaType'; -import { mediaTypeDisplayName } from '../utils/Utils'; -import Icon from './Icon'; - -interface PropertyMappingModelComponentProps { - model: PropertyMappingModelData; - save: (model: PropertyMappingModelData) => void; - /** When false, hides the media-type heading (e.g. modal title already shows it). Default true. */ - showMediaTypeTitle?: boolean; -} - -export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { - // Create a store from the model's plain data - const [modelData, setModelData] = createStore(props.model); - - // Derive the validation result reactively - const validationResult = createMemo(() => { - const model = PropertyMappingModel.fromJSON(modelData); - return model.validate(); - }); - - const persistIfValid = () => { - const model = PropertyMappingModel.fromJSON(modelData); - if (model.validate().res) { - props.save(model); - } - }; - - const showTitle = () => props.showMediaTypeTitle !== false; - - return ( -
- -
-
{mediaTypeDisplayName(modelData.type as MediaType)}
-
-
- - -
{validationResult().err?.message}
-
- -
- - - - - - - - - - - - {(property, index) => ( - - - - -
property cannot be remapped
- - } - > -
- - - - - - - )} - - -
PropertyMappingNew nameWikilink
- {property.property} - - - - —} - > -
- - { - setModelData('properties', index(), 'newProperty', e.currentTarget.value); - persistIfValid(); - }} - /> -
-
-
-
-
- ); -} +import { createMemo, For, Show } from 'solid-js'; +import { createStore } from 'solid-js/store'; +import { PropertyMappingModel, PropertyMappingOption, propertyMappingOptions, type PropertyMappingModelData } from './PropertyMapping'; +import type { MediaType } from '../utils/MediaType'; +import { mediaTypeDisplayName } from '../utils/Utils'; +import Icon from './Icon'; + +interface PropertyMappingModelComponentProps { + model: PropertyMappingModelData; + save: (model: PropertyMappingModelData) => void; + /** When false, hides the media-type heading (e.g. modal title already shows it). Default true. */ + showMediaTypeTitle?: boolean; +} + +export default function PropertyMappingModelComponent(props: PropertyMappingModelComponentProps) { + // Create a store from the model's plain data + const [modelData, setModelData] = createStore(props.model); + + // Derive the validation result reactively + const validationResult = createMemo(() => { + const model = PropertyMappingModel.fromJSON(modelData); + return model.validate(); + }); + + let draggedIndex: number | null = null; + + const onDragStart = (e: DragEvent, index: number) => { + draggedIndex = index; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + // Firefox requires data to be set to drag + e.dataTransfer.setData('text/plain', index.toString()); + } + }; + + const onDragOver = (e: DragEvent, index: number) => { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'move'; + } + }; + + const onDrop = (e: DragEvent, dropIndex: number) => { + e.preventDefault(); + if (draggedIndex === null || draggedIndex === dropIndex) { + draggedIndex = null; + return; + } + + const newProperties = [...modelData.properties]; + const item = newProperties.splice(draggedIndex, 1)[0]; + newProperties.splice(dropIndex, 0, item); + + setModelData('properties', newProperties); + persistIfValid(); + draggedIndex = null; + }; + + const persistIfValid = () => { + const model = PropertyMappingModel.fromJSON(modelData); + if (model.validate().res) { + props.save(model); + } + }; + + const showTitle = () => props.showMediaTypeTitle !== false; + + return ( +
+ +
+
{mediaTypeDisplayName(modelData.type as MediaType)}
+
+
+ + +
{validationResult().err?.message}
+
+ +
+ + + + + + + + + + + + + + + + {(property, index) => ( + onDragStart(e, index())} + onDragOver={(e) => onDragOver(e, index())} + onDrop={(e) => onDrop(e, index())} + style={{ + cursor: property.locked ? 'default' : 'grab', + }} + > + + + + +
property cannot be remapped
+ + } + > +
+ + + + + + + + + + + + + )} + + +
PropertyMappingNew nameTagWikilinkPin
+ + + ≡ + + + + {property.property} + + + + —} + > +
+ + { + setModelData('properties', index(), 'newProperty', e.currentTarget.value); + persistIfValid(); + }} + /> +
+
+
+ + { + setModelData('properties', index(), 'autoTagPrefix', e.currentTarget.value); + persistIfValid(); + }} + /> + + + + + +
+
+
+ ); +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 8b62bb28..92c2bcb9 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -2,18 +2,17 @@ import type { App, IconName } from 'obsidian'; import { Platform, PluginSettingTab, SecretComponent, SettingGroup, setIcon } from 'obsidian'; import { MediaType } from 'src/utils/MediaType'; import type MediaDbPlugin from '../main'; -import { ApiSecretID } from './apiSecretsHelper'; import { PropertyMappingModal } from '../modals/PropertyMappingModal'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { noteTypeValueForMedia, setNoteTypeForMedia } from '../utils/noteTypeSettings'; import { fragWithHTML, mediaTypeDisplayName, unCamelCase } from '../utils/Utils'; +import { ApiSecretID } from './apiSecretsHelper'; import type { PropertyMappingModelData } from './PropertyMapping'; import { PropertyMapping, PropertyMappingModel, PropertyMappingOption } from './PropertyMapping'; import { FileSuggest } from './suggesters/FileSuggest'; import { FolderSuggest } from './suggesters/FolderSuggest'; - function mediaTypeTabIcon(mediaType: MediaType): IconName { switch (mediaType) { case MediaType.Artist: @@ -60,6 +59,8 @@ export interface MediaDbPluginSettings { autoTagProperties: string; enableWikiLinkParsing: boolean; autoUpdateAiringMode: boolean; + addNormalizeTitlesAsAlias: boolean; + useObjectFormatForCurrencyValues: boolean; BoardgameGeekAPI_disabledMediaTypes: MediaType[]; ComicVineAPI_disabledMediaTypes: MediaType[]; @@ -126,8 +127,12 @@ export interface MediaDbPluginSettings { songNoteType: string; boardgameNoteType: string; bookNoteType: string; + /** When true, importing an artist also creates album and song notes from their discography. */ + artistAutomaticallyImportReleases: boolean; /** When true, artist discography import nests albums and songs under artistFolder/ArtistName/… instead of using album/song import folders. */ artistUseFileTreeForSongs: boolean; + /** When true, each imported album also creates a note per track (standalone album import or artist discography). */ + musicReleaseAutomaticallyImportSongs: boolean; boardgameFolder: string; bookFolder: string; @@ -373,6 +378,8 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { enableWikiLinkParsing: false, autoUpdateAiringMode: false, tmdbRegion: 'US', + addNormalizeTitlesAsAlias: false, + useObjectFormatForCurrencyValues: false, BoardgameGeekAPI_disabledMediaTypes: [], ComicVineAPI_disabledMediaTypes: [], @@ -426,7 +433,9 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { musicReleaseFolder: 'Media DB/music', artistFolder: 'Media DB/artists', songFolder: 'Media DB/music/songs', + artistAutomaticallyImportReleases: true, artistUseFileTreeForSongs: false, + musicReleaseAutomaticallyImportSongs: true, boardgameFolder: 'Media DB/boardgames', bookFolder: 'Media DB/books', @@ -516,21 +525,20 @@ export class MediaDbSettingTab extends PluginSettingTab { } private addApiSecretSetting(group: SettingGroup, name: string, description: string, slot: ApiSecretID): void { - group.addSetting( - setting => - setting - .setName(name) - .setDesc(description) - .addComponent(el => { - const component = new SecretComponent(this.app, el); - const { linkedApiSecretIds } = this.plugin.settings; - const linkedId = linkedApiSecretIds[slot] ?? ''; - component.setValue(linkedId).onChange((secretId: string) => { - linkedApiSecretIds[slot] = secretId; - this.plugin.saveSettings(); - }); - return component; - }), + group.addSetting(setting => + setting + .setName(name) + .setDesc(description) + .addComponent(el => { + const component = new SecretComponent(this.app, el); + const { linkedApiSecretIds } = this.plugin.settings; + const linkedId = linkedApiSecretIds[slot] ?? ''; + component.setValue(linkedId).onChange((secretId: string) => { + linkedApiSecretIds[slot] = secretId; + this.plugin.saveSettings(); + }); + return component; + }), ); } @@ -587,9 +595,7 @@ export class MediaDbSettingTab extends PluginSettingTab { setting => void setting .setName('Note type') - .setDesc( - `Value for the "type" field in frontmatter. Leave blank to use the default (${mediaType}).`, - ) + .setDesc(`Value for the "type" field in frontmatter. Leave blank to use the default (${mediaType}).`) .addText(cb => { cb.setPlaceholder(String(mediaType)) .setValue(mediaTypeSetting.getNoteType(this.plugin.settings)) @@ -670,16 +676,17 @@ export class MediaDbSettingTab extends PluginSettingTab { options?.appendToSection?.(mediaTypeGroup); if (this.plugin.settings.useDefaultFrontMatter) { - mediaTypeGroup.addSetting(setting => - void setting - .setName('Property mappings') - .setDesc(`How metadata fields map to frontmatter for ${descNoun} notes.`) - .addButton(btn => { - btn.setButtonText('Edit'); - btn.onClick(() => { - new PropertyMappingModal(this.app, this.plugin, mediaType).open(); - }); - }), + mediaTypeGroup.addSetting( + setting => + void setting + .setName('Property mappings') + .setDesc(`How metadata fields map to frontmatter for ${descNoun} notes.`) + .addButton(btn => { + btn.setButtonText('Edit'); + btn.onClick(() => { + new PropertyMappingModal(this.app, this.plugin, mediaType).open(); + }); + }), ); } } @@ -693,13 +700,23 @@ export class MediaDbSettingTab extends PluginSettingTab { this.renderMediaTypeSection(panel, byType(MediaType.Artist), mediaTypeApiMap, { sectionHeading: 'Artist', appendToSection: group => { + group.addSetting( + setting => + void setting + .setName('Automatically Import Releases') + .setDesc('When importing an artist, also create notes for their studio albums and tracks.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.artistAutomaticallyImportReleases).onChange(data => { + this.plugin.settings.artistAutomaticallyImportReleases = data; + void this.plugin.saveSettings(); + }); + }), + ); group.addSetting( setting => void setting .setName('Use file trees for songs') - .setDesc( - 'Use a file tree hierarchy to store albums and songs for each artist.', - ) + .setDesc('Use a file tree hierarchy to store albums and songs for each artist.') .addToggle(cb => { cb.setValue(this.plugin.settings.artistUseFileTreeForSongs).onChange(data => { this.plugin.settings.artistUseFileTreeForSongs = data; @@ -714,6 +731,20 @@ export class MediaDbSettingTab extends PluginSettingTab { this.renderMediaTypeSection(panel, byType(MediaType.MusicRelease), mediaTypeApiMap, { sectionHeading: 'Album', hideImportFolder: fileTree, + appendToSection: group => { + group.addSetting( + setting => + void setting + .setName('Automatically Import Songs') + .setDesc('When importing an album (on its own or as part of an artist import), also create a note for each track.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.musicReleaseAutomaticallyImportSongs).onChange(data => { + this.plugin.settings.musicReleaseAutomaticallyImportSongs = data; + void this.plugin.saveSettings(); + }); + }), + ); + }, }); panel.createDiv({ cls: 'media-db-plugin-spacer' }); this.renderMediaTypeSection(panel, byType(MediaType.Song), mediaTypeApiMap, { @@ -857,7 +888,7 @@ export class MediaDbSettingTab extends PluginSettingTab { "For more syntax, refer to format reference.
" + "Your current syntax looks like this: " + this.plugin.dateFormatter.getPreview() + - "", + '', ), ) .addText(cb => { @@ -906,7 +937,9 @@ export class MediaDbSettingTab extends PluginSettingTab { setting => void setting .setName('Enable Templater integration') - .setDesc('Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.') + .setDesc( + 'Enable integration with the templater plugin, this also needs templater to be installed. Warning: Templater allows you to execute arbitrary JavaScript code and system commands.', + ) .addToggle(cb => { cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { this.plugin.settings.enableTemplaterIntegration = data; @@ -950,8 +983,36 @@ export class MediaDbSettingTab extends PluginSettingTab { }), ); - panel.createEl('h3', { text: 'Auto-Tracker' }).style.marginTop = '1.5em'; - const autoTrackerGroup = new SettingGroup(panel); + generalGroup.addSetting( + setting => + void setting + .setName('Add Normalized Titles as Alias') + .setDesc('If the title contains non-ASCII characters, add a normalized ASCII version of the title in aliases.') + .addToggle(cb => { + cb.setValue(this.plugin.settings.addNormalizeTitlesAsAlias).onChange(data => { + this.plugin.settings.addNormalizeTitlesAsAlias = data; + void this.plugin.saveSettings(); + }); + }), + ); + + generalGroup.addSetting( + setting => + void setting + .setName('Use object format for currency values') + .setDesc( + 'For movies, store Budget and Revenue as nested objects with numeric value and currency (e.g. USD) instead of a single formatted string.', + ) + .addToggle(cb => { + cb.setValue(this.plugin.settings.useObjectFormatForCurrencyValues).onChange(data => { + this.plugin.settings.useObjectFormatForCurrencyValues = data; + void this.plugin.saveSettings(); + }); + }), + ); + + panel.createEl('h3', { text: 'Auto-Tracker' }).style.marginTop = '1.5em'; + const autoTrackerGroup = new SettingGroup(panel); autoTrackerGroup.addSetting( setting => @@ -989,39 +1050,7 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.settings.autoTrackerReleasedKey = data.trim() || 'released'; void this.plugin.saveSettings(); }); - }), - ); - - panel.createEl('h3', { text: 'Auto-Tag Properties' }).style.marginTop = '1.5em'; - const autoTagGroup = new SettingGroup(panel); - - autoTagGroup.addSetting( - setting => - void setting - .setName('Enable Auto Tagging') - .setDesc('Feature to automatically sanitize properties into standard Obsidian tags.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.enableAutoTagging).onChange(data => { - this.plugin.settings.enableAutoTagging = data; - void this.plugin.saveSettings(); - }); - }), - ); - - autoTagGroup.addSetting( - setting => - void setting - .setName('Auto-Tag whitelisted properties') - .setDesc('Comma separated list of property names. If a property in this list is present, its values will be sanitized and appended to the Obsidian native `tags` array.') - .addText(text => - text - .setPlaceholder('genres, platforms') - .setValue(this.plugin.settings.autoTagProperties) - .onChange(async value => { - this.plugin.settings.autoTagProperties = value; - await this.plugin.saveSettings(); - }), - ), + }), ); }); @@ -1039,7 +1068,9 @@ export class MediaDbSettingTab extends PluginSettingTab { s => void s .setName('Wiki-Link parsing') - .setDesc('When enabled, properties listed below are formatted as Obsidian [[Wiki-Links]] across ALL media types globally. This complements the per-property wikilink checkbox in Property Mappings, which only affects that specific property.') + .setDesc( + 'When enabled, properties listed below are formatted as Obsidian [[Wiki-Links]] across ALL media types globally. This complements the per-property wikilink checkbox in Property Mappings, which only affects that specific property.', + ) .addToggle(cb => { cb.setValue(this.plugin.settings.enableWikiLinkParsing).onChange(data => { this.plugin.settings.enableWikiLinkParsing = data; @@ -1051,7 +1082,9 @@ export class MediaDbSettingTab extends PluginSettingTab { s => void s .setName('Wiki-Link properties') - .setDesc('Comma-separated property names to convert to [[Wiki-Links]] for ALL media types. Use this for custom or cross-type properties (e.g. storefront, launcher). For standard properties like genres or studio, the wikilink checkbox inside Property Mappings also works.') + .setDesc( + 'Comma-separated property names to convert to [[Wiki-Links]] for ALL media types. Use this for custom or cross-type properties (e.g. storefront, launcher). For standard properties like genres or studio, the wikilink checkbox inside Property Mappings also works.', + ) .addTextArea(cb => { cb.setPlaceholder('genres, storefront, category') .setValue(this.plugin.settings.autoTagEntities) diff --git a/src/settings/apiSecretsHelper.ts b/src/settings/apiSecretsHelper.ts index b20908b7..8729b9ac 100644 --- a/src/settings/apiSecretsHelper.ts +++ b/src/settings/apiSecretsHelper.ts @@ -6,19 +6,19 @@ import type { App } from 'obsidian'; * @see https://docs.obsidian.md/plugins/guides/secret-storage */ export enum ApiSecretID { - omdb, - tmdb, - mobyGames, - giantBomb, - igdbClientId, - igdbClientSecret, - rawg, - comicVine, - boardgameGeek, - genius, - /** Spotify Developer Dashboard — used when MusicBrainz has no streaming URL for a recording. */ - spotifyClientId, - spotifyClientSecret, + omdb, + tmdb, + mobyGames, + giantBomb, + igdbClientId, + igdbClientSecret, + rawg, + comicVine, + boardgameGeek, + genius, + /** Spotify Developer Dashboard — used when MusicBrainz has no streaming URL for a recording. */ + spotifyClientId, + spotifyClientSecret, } export function getApiSecretValue(app: App, linked: Record | undefined, slot: ApiSecretID): string { diff --git a/src/styles.css b/src/styles.css index 5c0c2e6a..caf54cc3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -41,6 +41,46 @@ small.media-db-plugin-list-text { font-size: 16px; } +.media-db-plugin-select-element-flex { + display: flex; + gap: 10px; + align-items: flex-start; +} + +.media-db-plugin-select-thumb { + width: 48px; + height: 72px; + flex: 0 0 48px; + background: var(--background-modifier-hover); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + overflow: hidden; +} + +.media-db-plugin-select-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.media-db-plugin-select-content { + flex: 1; + min-width: 0; +} + +.media-db-plugin-select-title { + font-weight: 600; +} + +.media-db-plugin-select-thumb span { + color: var(--text-muted); + font-size: 12px; + text-align: center; +} + .media-db-plugin-select-element-selected { border-left: 5px solid var(--interactive-accent) !important; background: var(--background-secondary-alt); @@ -149,9 +189,15 @@ small.media-db-plugin-list-text { overflow-x: auto; } +/* Widen the property-mapping popup to fit all columns comfortably */ +.modal:has(.media-db-plugin-property-mappings-table) { + width: min(92vw, 750px) !important; +} + .media-db-plugin-property-mappings-table { width: 100%; border-collapse: collapse; + table-layout: fixed; border-spacing: 0; font-size: var(--font-ui-small); } @@ -186,22 +232,64 @@ small.media-db-plugin-list-text { border-bottom: none; } +.col-drag { + width: 4%; + text-align: center; + vertical-align: middle; +} + .col-property { - width: 25%; + width: 19%; white-space: nowrap; } .col-mapping { - width: 20%; + width: 12%; } .col-new-name { - width: 40%; + width: 21%; } .col-wikilink { + width: 11%; + text-align: center !important; + vertical-align: middle; + white-space: nowrap; +} + +.col-pin { + width: 9%; + text-align: center !important; + vertical-align: middle; + white-space: nowrap; +} + +.col-tag { + width: 9%; + text-align: center !important; + vertical-align: middle; + white-space: nowrap; + padding-left: 2px !important; +} + +.col-tag-prefix { width: 15%; - text-align: center; + vertical-align: middle; + padding-right: 4px !important; +} + +.media-db-plugin-tag-prefix-input { + width: calc(100% + 14px); + box-sizing: border-box; + margin-right: -14px; + font-size: var(--font-ui-smaller); + padding: 2px 4px; + border-radius: var(--radius-s); + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); + color: var(--text-normal); + min-width: 0; } .col-locked { @@ -248,18 +336,21 @@ small.media-db-plugin-list-text { font-size: var(--font-ui-medium); } -.media-db-plugin-property-mapping-wikilink-label { - display: inline-flex; - align-items: center; - justify-content: center; +.media-db-plugin-property-mapping-wikilink-label, +.media-db-plugin-property-mapping-pin-label { + display: inline-block; cursor: pointer; - padding: var(--size-4-1); + padding: 0; + margin: 0; + line-height: 0; } -.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'] { +.media-db-plugin-property-mapping-wikilink-label input[type='checkbox'], +.media-db-plugin-property-mapping-pin-label input[type='checkbox'] { cursor: pointer; width: var(--checkbox-size); height: var(--checkbox-size); + margin: 0; } /* @@ -414,9 +505,15 @@ small.media-db-plugin-list-text { font-size: 0.95em; } -.mdb-stat-success { color: var(--color-green); } -.mdb-stat-error { color: var(--color-red); } -.mdb-stat-skipped { color: var(--text-muted); } +.mdb-stat-success { + color: var(--color-green); +} +.mdb-stat-error { + color: var(--color-red); +} +.mdb-stat-skipped { + color: var(--text-muted); +} .mdb-completion-notes { margin-bottom: 20px; diff --git a/src/utils/AutoTrackerHelper.ts b/src/utils/AutoTrackerHelper.ts index 7155c9dd..28628a07 100644 --- a/src/utils/AutoTrackerHelper.ts +++ b/src/utils/AutoTrackerHelper.ts @@ -1,4 +1,5 @@ -import { Notice, TFile, TFolder } from 'obsidian'; +import type { TFile, TFolder } from 'obsidian'; +import { Notice } from 'obsidian'; import type MediaDbPlugin from 'src/main'; import { CompletionModal } from 'src/modals/CompletionModal'; import { dateTimeToString, markdownTable } from './Utils'; @@ -21,15 +22,13 @@ export class AutoTrackerHelper { } async runAutoUpdate(silent: boolean = false, targetFolder?: TFolder): Promise { - const allFiles = targetFolder - ? this.plugin.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(targetFolder.path)) - : this.plugin.app.vault.getMarkdownFiles(); + const allFiles = targetFolder ? this.plugin.app.vault.getMarkdownFiles().filter(f => f.path.startsWith(targetFolder.path)) : this.plugin.app.vault.getMarkdownFiles(); const filesToUpdate: TFile[] = []; for (const file of allFiles) { const metadata = this.plugin.getMetadataFromFileCache(file); - if (metadata && metadata.dataSource && metadata.id) { + if (metadata?.dataSource && metadata.id) { const airingKey = this.plugin.settings.autoTrackerAiringKey; const releasedKey = this.plugin.settings.autoTrackerReleasedKey; if (metadata[airingKey] === true || metadata[releasedKey] === false) { @@ -54,7 +53,7 @@ export class AutoTrackerHelper { const startTime = Date.now(); let successCount = 0; let failCount = 0; - const erroredFiles: { filePath: string, error: string }[] = []; + const erroredFiles: { filePath: string; error: string }[] = []; for (const file of filesToUpdate) { try { diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts index ffbe88dd..eeda4cae 100644 --- a/src/utils/BulkImportHelper.ts +++ b/src/utils/BulkImportHelper.ts @@ -1,9 +1,9 @@ import type { TFolder } from 'obsidian'; import { TFile } from 'obsidian'; import type MediaDbPlugin from 'src/main'; +import { CompletionModal } from 'src/modals/CompletionModal'; import { MediaDbBulkImportModal as MediaDbBulkImportModal } from 'src/modals/MediaDbBulkImportModal'; import type { MediaTypeModel } from 'src/models/MediaTypeModel'; -import { CompletionModal } from 'src/modals/CompletionModal'; import { ModalResultCode } from './ModalHelper'; import { dateTimeToString, markdownTable } from './Utils'; diff --git a/src/utils/BulkRecreateHelper.ts b/src/utils/BulkRecreateHelper.ts new file mode 100644 index 00000000..24844da9 --- /dev/null +++ b/src/utils/BulkRecreateHelper.ts @@ -0,0 +1,69 @@ +import type { TFolder } from 'obsidian'; +import { TFile, Notice } from 'obsidian'; +import type MediaDbPlugin from 'src/main'; +import { BulkRecreateConfirmModal, type BulkRecreateMode } from 'src/modals/BulkRecreateConfirmModal'; +import { CompletionModal } from 'src/modals/CompletionModal'; +import { dateTimeToString, markdownTable } from './Utils'; + +export class BulkRecreateHelper { + readonly plugin: MediaDbPlugin; + + constructor(plugin: MediaDbPlugin) { + this.plugin = plugin; + } + + async recreateFolder(folder: TFolder): Promise { + const mediaFiles = folder.children.filter((child): child is TFile => { + if (!(child instanceof TFile)) return false; + const metadata = this.plugin.getMetadataFromFileCache(child); + return Boolean(metadata?.dataSource && metadata.id); + }); + + if (mediaFiles.length === 0) { + new Notice('MDB | No Media DB files found in this folder.'); + return; + } + + new BulkRecreateConfirmModal(this.plugin.app, async (mode: BulkRecreateMode, silent: boolean) => { + // 'reorder' = only metadata (keeps user values, re-applies property order + pin) + // 'full' = full recreate with template (resets custom values) + const onlyMetadata = mode === 'reorder'; + + new Notice(`MDB | Bulk recreating ${mediaFiles.length} files (mode: ${mode}). Please wait...`); + const startTime = Date.now(); + let successCount = 0; + let failCount = 0; + const erroredFiles: { filePath: string; error: string }[] = []; + + for (const file of mediaFiles) { + try { + await this.plugin.updateNote(file, onlyMetadata, false, silent); + successCount++; + } catch (e) { + console.error(`MDB | Failed to bulk recreate ${file.path}: `, e); + failCount++; + erroredFiles.push({ filePath: file.path, error: `${e}` }); + } + await new Promise(resolve => setTimeout(resolve, 800)); + } + + if (failCount > 0 && erroredFiles.length > 0) { + const title = `MDB - bulk recreate error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; + const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); + const fileContent = markdownTable(table); + await this.plugin.app.vault.create(filePath, fileContent); + } + + new CompletionModal(this.plugin.app, { + title: 'Bulk Recreate Complete', + icon: 'file-stack', + total: mediaFiles.length, + success: successCount, + errors: failCount, + elapsedMs: Date.now() - startTime, + notes: failCount > 0 ? ['Some files could not be recreated. A detailed report file has been created in your vault folder.'] : [], + }).open(); + }).open(); + } +} diff --git a/src/utils/BulkUpdateHelper.ts b/src/utils/BulkUpdateHelper.ts index d5c37db3..5396f327 100644 --- a/src/utils/BulkUpdateHelper.ts +++ b/src/utils/BulkUpdateHelper.ts @@ -1,4 +1,5 @@ -import { TFolder, TFile, Notice } from 'obsidian'; +import type { TFolder} from 'obsidian'; +import { TFile, Notice } from 'obsidian'; import type MediaDbPlugin from 'src/main'; import { BulkUpdateConfirmModal } from 'src/modals/BulkUpdateConfirmModal'; import { CompletionModal } from 'src/modals/CompletionModal'; @@ -15,7 +16,7 @@ export class BulkUpdateHelper { const mediaFiles = folder.children.filter((child): child is TFile => { if (!(child instanceof TFile)) return false; const metadata = this.plugin.getMetadataFromFileCache(child); - return Boolean(metadata && metadata.dataSource && metadata.id); + return Boolean(metadata?.dataSource && metadata.id); }); if (mediaFiles.length === 0) { @@ -28,7 +29,7 @@ export class BulkUpdateHelper { const startTime = Date.now(); let successCount = 0; let failCount = 0; - const erroredFiles: { filePath: string, error: string }[] = []; + const erroredFiles: { filePath: string; error: string }[] = []; for (const file of mediaFiles) { try { diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 3b51f8b3..eb20d1e6 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -1,5 +1,5 @@ import type { App, TFile } from 'obsidian'; -import { TFolder } from 'obsidian'; +import { normalizePath, TFolder } from 'obsidian'; import { ArtistModel } from '../models/ArtistModel'; import { BoardGameModel } from '../models/BoardGameModel'; import { BookModel } from '../models/BookModel'; @@ -15,7 +15,7 @@ import { WikiModel } from '../models/WikiModel'; import type { MediaDbPluginSettings } from '../settings/Settings'; import { ILLEGAL_FILENAME_CHARACTERS } from './IllegalFilenameCharactersList'; import { MediaType } from './MediaType'; -import { replaceTags } from './Utils'; +import { replaceIllegalFileNameCharactersInString, replaceTags } from './Utils'; // All media types in alphabetical order export const MEDIA_TYPES: MediaType[] = [ @@ -98,6 +98,19 @@ export class MediaTypeManager { return cleanedFileName.replaceAll(/ +/g, ' '); } + /** Expands {{ tags }} in a folder path and sanitizes each segment for vault paths. */ + expandFolderPathForModel(folderPath: string, mediaTypeModel: MediaTypeModel): string { + const expanded = replaceTags(folderPath, mediaTypeModel, true); + const segments = expanded + .split('/') + .map(seg => replaceIllegalFileNameCharactersInString(seg).replaceAll(/ +/g, ' ').trim()) + .filter(seg => seg.length > 0); + if (segments.length === 0) { + return '/'; + } + return normalizePath(segments.join('/')); + } + async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise { const templateFilePath = this.mediaTemplateMap.get(mediaTypeModel.getMediaType()); @@ -129,7 +142,7 @@ export class MediaTypeManager { let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); folderPath ??= `/`; - // console.log(folderPath); + folderPath = this.expandFolderPathForModel(folderPath, mediaTypeModel); if (!(await app.vault.adapter.exists(folderPath))) { await app.vault.createFolder(folderPath); diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index b4a16b99..375c144a 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -385,3 +385,15 @@ export function getLanguageName(code: string): string | null { return language?.name ?? null; } + +export function normalizeTitleForAsciiAlias(title: string): string | null { + const normalized = title.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + if (normalized !== title) return normalized; + return null; +} + +export function parseUsdWholeDollarsFromDisplayString(value: string): number | null { + const cleaned = value.replace(/[^0-9]/g, ''); + if (cleaned) return parseInt(cleaned, 10); + return null; +} diff --git a/src/utils/noteTypeSettings.ts b/src/utils/noteTypeSettings.ts index 31b51d70..71b32f82 100644 --- a/src/utils/noteTypeSettings.ts +++ b/src/utils/noteTypeSettings.ts @@ -1,6 +1,6 @@ import type { MediaDbPluginSettings } from '../settings/Settings'; -import { MEDIA_TYPES } from './MediaTypeManager'; import { MediaType } from './MediaType'; +import { MEDIA_TYPES } from './MediaTypeManager'; const MEDIA_TYPE_TO_NOTE_TYPE_KEY: Record = { [MediaType.Artist]: 'artistNoteType', @@ -35,10 +35,7 @@ export function setNoteTypeForMedia(settings: MediaDbPluginSettings, mediaType: /** * Maps a frontmatter `type` string (legacy enum id or configured custom string) to {@link MediaType}. */ -export function resolveMetadataTypeToMediaType( - settings: MediaDbPluginSettings, - noteType: unknown, -): MediaType | undefined { +export function resolveMetadataTypeToMediaType(settings: MediaDbPluginSettings, noteType: unknown): MediaType | undefined { if (noteType === undefined || noteType === null) { return undefined; } From 3ca124aa1f220f17fcb313e2c812c980465237fb Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:21:01 +0300 Subject: [PATCH 45/60] Add files via upload From 478103d8ba79e9d2a61be2442e393f9529a44c2a Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:27:38 +0300 Subject: [PATCH 46/60] Add files via upload --- README.md | 80 ++++++++++++++----------------------------------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 72a6dc6b..b37ca3f8 100644 --- a/README.md +++ b/README.md @@ -44,76 +44,36 @@ 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. +- Includes a sophisticated **Emergency Abort Mechanism**: You can cancel any ongoing bulk update or background tracker safely at any time. -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. - -#### Intelligent Wiki-Link Generation - -Automatically format specific metadata fields into Obsidian Wiki-Links based on a customizable whitelist. - -- You can specify comma-separated properties (like `genres, publishers, storefront`) in the plugin settings. -- The plugin will parse both API-fetched arrays and your own custom manual properties, formatting them safely as `[[folder/value|value]]`. -- If you disable the feature later, updating the note will cleanly strip the brackets and revert the entries back to plain text. - -#### Auto-Tagging Engine & Ghost Tag Purging - -Generate clean, sanitized hashtags automatically (e.g. `#role-playing-rpg`) from mapped array properties or Wiki-Links. - -- The engine creates tags without destroying any manual tags you have explicitly typed into your notes. -- **Ghost Tag Purging**: Whenever a piece of media updates (e.g. a game's genre changes on IGDB), the plugin intelligently calculates which old tags it previously generated and safely removes them, leaving your manual user tags perfectly preserved. - -#### Background Auto-Tracker - -An automated, non-blocking background scanner that actively searches your vault for ongoing series (Airing) or unreleased games/movies and queues them for silent metadata updates. - -- Scans trigger once on Obsidian startup or can be triggered manually from the left ribbon. -- Includes a sophisticated **Emergency Abort Mechanism**: At any point during a bulk process or background tracker queue, you can click the Ribbon Icon or use the `Cancel All Updates` button inside the Overwrite Modal to instantly halt the entire queue gracefully without restarting Obsidian. - -The plugin allows you to import your preexisting media collection and upgrade it to Media DB entries. - -##### Prerequisites - -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. - -##### Importing - -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. - -Then the plugin will go through every file in the folder and prompt you to select from the search results. - -##### 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 From 837647ee45b544ed2c340da67b25afd687f6d123 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:28:55 +0300 Subject: [PATCH 47/60] Fix formatting in credits section of README From 423da0d94f0d47a43849e4b4c21ed6e05b002521 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:32:01 +0300 Subject: [PATCH 48/60] Add files via upload --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index b37ca3f8..66ff12df 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ An automated tracking system that periodically checks for `airing` and `released - 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. -- Includes a sophisticated **Emergency Abort Mechanism**: You can cancel any ongoing bulk update or background tracker safely at any time. #### Intelligent Ghost Tag Purging From 17320a02f249fe4dc2a95b8a2c3d515962fc5eb0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:32:14 +0300 Subject: [PATCH 49/60] Fix formatting in credits section of README From 90775d6e91a77fc17c3fc03930cf203683b7b854 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:22:27 +0300 Subject: [PATCH 50/60] Add files via upload --- src/main.ts | 62 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/main.ts b/src/main.ts index a1618e7e..ee23a4e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -179,7 +179,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-bulk-import-active-file-folder', - name: 'Media DB: Bulk Import Folder (Active Context)', + name: 'Bulk Import Folder (Active Context)', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile?.parent) return false; @@ -190,7 +190,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-bulk-recreate-active-file-folder', - name: 'Media DB: Bulk Recreate Notes (Active Context)', + name: 'Bulk Recreate Notes (Active Context)', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile?.parent) return false; @@ -201,7 +201,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-bulk-update-active-file-folder', - name: 'Media DB: Bulk Update Metadata (Active Context)', + name: 'Bulk Update Metadata (Active Context)', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile?.parent) return false; @@ -212,7 +212,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-download-images-active-file-folder', - name: 'Media DB: Download images in folder (Active Context)', + name: 'Download images in folder (Active Context)', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile?.parent) return false; @@ -223,7 +223,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-download-images-active-note', - name: 'Media DB: Download images in active note', + name: 'Download images in active note', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile?.extension !== 'md') return false; @@ -234,7 +234,7 @@ export default class MediaDbPlugin extends Plugin { this.addCommand({ id: 'media-db-manual-sync-auto-tracker', - name: 'Media DB: Force Auto-Tracker Background Scan', + name: 'Force Auto-Tracker Background Scan', callback: () => this.autoTrackerHelper.startBackgroundScan(false), }); @@ -265,7 +265,7 @@ export default class MediaDbPlugin extends Plugin { // register command to update the open note this.addCommand({ id: 'update-media-db-note', - name: 'Update open note (this will recreate the note)', + name: 'Recreate open note (Reset mode)', checkCallback: (checking: boolean) => { if (!this.app.workspace.getActiveFile()) { return false; @@ -278,13 +278,27 @@ export default class MediaDbPlugin extends Plugin { }); this.addCommand({ id: 'update-media-db-note-metadata', - name: 'Update metadata', + name: 'Recreate open note (Safe mode)', checkCallback: (checking: boolean) => { if (!this.app.workspace.getActiveFile()) { return false; } if (!checking) { - void this.updateActiveNote(true); + void this.updateActiveNote(true, false); + } + return true; + }, + }); + + this.addCommand({ + id: 'update-media-db-note-legacy', + name: 'Update metadata (Keep current property order)', + checkCallback: (checking: boolean) => { + if (!this.app.workspace.getActiveFile()) { + return false; + } + if (!checking) { + void this.updateActiveNote(true, true); } return true; }, @@ -1101,7 +1115,7 @@ export default class MediaDbPlugin extends Plugin { let fileContent = ''; template = options.attachTemplate ? template : ''; - ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile)); + ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile, options.preservePropertyOrder)); ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- @@ -1226,7 +1240,7 @@ export default class MediaDbPlugin extends Plugin { return allTags.map(t => String(t).trim()).filter(t => t && !autoTagValues.has(t.toLowerCase()) && !t.toLowerCase().startsWith('mediadb/')); } - async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile, preservePropertyOrder?: boolean): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!fileToAttach) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } @@ -1245,8 +1259,22 @@ export default class MediaDbPlugin extends Plugin { const oldManualTags = this.extractManualTags(attachFileMetadata, autoTagEntries); const oldAliases = rescueArray('aliases'); - // TODO: better object merging - fileMetadata = Object.assign(attachFileMetadata, fileMetadata); + if (preservePropertyOrder) { + // Messy legacy behavior: old attachFileMetadata acts as the base, preserving its currently unordered key layout + fileMetadata = Object.assign(attachFileMetadata, fileMetadata); + } else { + // Enforce strict property order from the new mapping + const orderedMetadata: Record = {}; + for (const key of Object.keys(fileMetadata)) { + orderedMetadata[key] = fileMetadata[key]; + } + for (const [key, value] of Object.entries(attachFileMetadata)) { + if (!(key in orderedMetadata)) { + orderedMetadata[key] = value; + } + } + fileMetadata = orderedMetadata; + } // Merge tags cleanly (Preserving only manual user tags, discarding old ghost auto-tags!) const newObjTags = fileMetadata.tags; @@ -1417,15 +1445,15 @@ export default class MediaDbPlugin extends Plugin { * Update the active note by querying the API again. * Tries to read the type and id of the active note (and dataSource when required). If successful it will query the api, delete the old note and create a new one. */ - async updateActiveNote(onlyMetadata: boolean = false): Promise { + async updateActiveNote(onlyMetadata: boolean = false, preserveOrder: boolean = false): Promise { const activeFile = this.app.workspace.getActiveFile() ?? undefined; if (!activeFile) { throw new Error('MDB | there is no active note'); } - return this.updateNote(activeFile, onlyMetadata, true, false); + return this.updateNote(activeFile, onlyMetadata, preserveOrder, true, false); } - async updateNote(activeFile: TFile, onlyMetadata: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { + async updateNote(activeFile: TFile, onlyMetadata: boolean = false, preserveOrder: boolean = false, openNoteFinal: boolean = true, overwrite: boolean = false): Promise { let metadata = this.getMetadataFromFileCache(activeFile); metadata = this.modelPropertyMapper.convertObjectBack(metadata); @@ -1462,7 +1490,7 @@ export default class MediaDbPlugin extends Plugin { console.debug(`MDB | newMediaTypeModel after merge`, newMediaTypeModel); if (onlyMetadata) { - await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); + await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite, preservePropertyOrder: preserveOrder }); } else { await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachTemplate: true, folder: activeFile.parent ?? undefined, openNote: openNoteFinal, overwrite }); } From 5a6939e4108163f9bbfb9ac3944711efee626cfe Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:22:44 +0300 Subject: [PATCH 51/60] Add files via upload --- src/utils/Utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 375c144a..7138b8cb 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -207,6 +207,7 @@ export interface CreateNoteOptions { openNote?: boolean; folder?: TFolder; overwrite?: boolean; + preservePropertyOrder?: boolean; } /** Runtime in whole minutes (TMDB/OMDb/MAL). 0 when unknown. Parses legacy string frontmatter (e.g. "136 min", "2 hr 5 min"). */ From e1b49d3b1fd8fc5b9fad9422ea59bbd2d9a59ad7 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:18:05 +0300 Subject: [PATCH 52/60] Add files via upload --- src/api/APIManager.ts | 27 ++------ src/api/APIModel.ts | 10 +-- src/api/GeniusClient.ts | 4 +- src/api/apis/MusicBrainzAPI.ts | 7 ++ src/api/apis/MusicBrainzArtistAPI.ts | 7 ++ src/api/helpers/geniusLyricsExtract.ts | 89 ++++++++++++++++++++++++++ src/modals/CompletionModal.ts | 29 +++++---- src/modals/MediaDbSearchResultModal.ts | 5 +- src/modals/MediaDbSeasonSelectModal.ts | 11 +--- src/settings/PropertyMapper.ts | 74 ++------------------- src/styles.css | 16 +++++ src/utils/AutoTrackerHelper.ts | 2 +- src/utils/BulkImportHelper.ts | 2 +- src/utils/BulkUpdateHelper.ts | 2 +- src/utils/musicFormatHelper.ts | 63 ++++++++++++++++++ test/genius-lyrics.test.ts | 2 +- 16 files changed, 222 insertions(+), 128 deletions(-) create mode 100644 src/api/helpers/geniusLyricsExtract.ts create mode 100644 src/utils/musicFormatHelper.ts diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 76ffb171..d0f9a59d 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -2,7 +2,6 @@ import { Notice } from 'obsidian'; import type { MediaTypeModel } from '../models/MediaTypeModel'; import type { MediaType } from '../utils/MediaType'; import type { APIModel } from './APIModel'; -import { isMusicBrainzFamilyDataSource, musicBrainzRegisteredApiName, MUSICBRAINZ_NOTE_DATA_SOURCE } from './musicBrainzConstants'; export class APIManager { apis: APIModel[]; @@ -54,34 +53,16 @@ export class APIManager { * @param mediaType When set with a MusicBrainz family dataSource, selects which MusicBrainz API handles {@link getById}. */ async queryDetailedInfoById(id: string, apiName: string, mediaType?: MediaType): Promise { - const trimmed = apiName.trim(); - const effectiveApiName = trimmed === '' && mediaType !== undefined && musicBrainzRegisteredApiName(mediaType) ? MUSICBRAINZ_NOTE_DATA_SOURCE : trimmed || apiName; - - if (isMusicBrainzFamilyDataSource(effectiveApiName) && mediaType !== undefined) { - const registeredName = musicBrainzRegisteredApiName(mediaType); - if (registeredName) { - const api = this.getApiByName(registeredName); - if (api) { - try { - return await api.getById(id); - } catch (e) { - new Notice(`Error querying ${api.apiName}: ${e}`); - console.warn(e); - - return 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 === effectiveApiName) { + 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; } } diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index d15fdcb0..450b5435 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -29,17 +29,17 @@ export abstract class APIModel { 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. - * Subclasses can override this to apply API-specific file name templates - * (e.g. using an Artist file name for artist links). * - * @param _property the property key (e.g. 'artists', 'albumTitle') * @param value the raw string value to wrap - * @param _obj the full metadata object (for context) * @param folderPrefix the wiki-link folder prefix (e.g. 'Media DB/wiki/') */ - wikilinkValueFor(_property: string, value: string, _obj: Record, folderPrefix: string): string { + wikilinkValueFor(value: string, folderPrefix: string): string { const clean = value .replace(/^\[\[(.*?)\]\]$/, '$1') .split('|') diff --git a/src/api/GeniusClient.ts b/src/api/GeniusClient.ts index eb081dbd..50b93634 100644 --- a/src/api/GeniusClient.ts +++ b/src/api/GeniusClient.ts @@ -1,6 +1,6 @@ import { requestUrl } from 'obsidian'; import { contactEmail, mediaDbVersion, pluginName } from '../utils/Utils'; -import { extractLyricsFromGeniusHtml } from './geniusLyricsExtract'; +import { extractLyricsFromGeniusHtml } from './helpers/geniusLyricsExtract'; interface GeniusSearchHit { result: { @@ -17,8 +17,6 @@ interface GeniusSearchResponse { }; } -export { extractLyricsFromGeniusHtml }; - export class GeniusClient { private readonly accessToken: string | undefined; private readonly userAgent: string; diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index 7732fabe..058c120c 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -109,6 +109,13 @@ export class MusicBrainzAPI extends APIModel { this.types = [MediaType.MusicRelease]; } + canHandleDataSource(dataSource: string, mediaType?: import('../../utils/MediaType').MediaType): boolean { + if (dataSource.contains('MusicBrainz')) { + return mediaType === MediaType.MusicRelease || mediaType === MediaType.Song; + } + return dataSource === this.apiName; + } + async searchByTitle(title: string): Promise { console.log(`MDB | api "${this.apiName}" queried by Title`); diff --git a/src/api/apis/MusicBrainzArtistAPI.ts b/src/api/apis/MusicBrainzArtistAPI.ts index 151c51fa..09579c0b 100644 --- a/src/api/apis/MusicBrainzArtistAPI.ts +++ b/src/api/apis/MusicBrainzArtistAPI.ts @@ -89,6 +89,13 @@ export class MusicBrainzArtistAPI extends APIModel { this.types = [MediaType.Artist]; } + canHandleDataSource(dataSource: string, mediaType?: import('../../utils/MediaType').MediaType): boolean { + if (dataSource.contains('MusicBrainz')) { + return mediaType === MediaType.Artist; + } + return dataSource === this.apiName; + } + private mbHeaders(): Record { return { 'User-Agent': `${pluginName}/${mediaDbVersion} (${contactEmail})`, diff --git a/src/api/helpers/geniusLyricsExtract.ts b/src/api/helpers/geniusLyricsExtract.ts new file mode 100644 index 00000000..ecdc178a --- /dev/null +++ b/src/api/helpers/geniusLyricsExtract.ts @@ -0,0 +1,89 @@ +const LYRICS_CONTAINER_OPEN_RE = /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; + +function stripHtmlToPlainLyrics(fragment: string): string { + return fragment + .replace(//gi, '\n') + .replace(/<\/p>/gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/\n{3,}/g, '\n\n') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); +} + +/** Parses nested
blocks; the naive `*?
` regex stops at the first inner close tag. */ +function extractBalancedDivInnerHtml(html: string, contentStart: number): string { + let depth = 1; + let i = contentStart; + const openRe = //gi; + while (depth > 0) { + openRe.lastIndex = i; + closeRe.lastIndex = i; + const om = openRe.exec(html); + const cm = closeRe.exec(html); + if (!cm) { + break; + } + const oIdx = om ? om.index : Number.POSITIVE_INFINITY; + const cIdx = cm.index; + if (om && oIdx < cIdx) { + depth++; + i = om.index + om[0].length; + } else { + depth--; + if (depth === 0) { + return html.slice(contentStart, cIdx); + } + i = cm.index + cm[0].length; + } + } + return ''; +} + +function collectLyricsContainersRegex(html: string): string[] { + const chunks: string[] = []; + let m: RegExpExecArray | null; + LYRICS_CONTAINER_OPEN_RE.lastIndex = 0; + while ((m = LYRICS_CONTAINER_OPEN_RE.exec(html)) !== null) { + const inner = extractBalancedDivInnerHtml(html, m.index + m[0].length); + if (inner) { + chunks.push(inner); + } + } + return chunks; +} + +function extractOneContainerPlain(el: Element): string { + const clone = el.cloneNode(true) as Element; + clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(node => node.remove()); + return stripHtmlToPlainLyrics(clone.innerHTML); +} + +export function extractLyricsFromGeniusHtml(html: string): string { + let chunks: string[] = []; + try { + const doc = new DOMParser().parseFromString(html, 'text/html'); + doc.querySelectorAll('[data-lyrics-container="true"]').forEach(c => { + const plain = extractOneContainerPlain(c); + if (plain) { + chunks.push(plain); + } + }); + } catch { + chunks = []; + } + + if (chunks.length === 0) { + return ''; + } + + return chunks + .join('\n\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); +} diff --git a/src/modals/CompletionModal.ts b/src/modals/CompletionModal.ts index 10b1d958..362d192b 100644 --- a/src/modals/CompletionModal.ts +++ b/src/modals/CompletionModal.ts @@ -1,10 +1,10 @@ import type {App} from 'obsidian'; -import { Modal, ButtonComponent } from 'obsidian'; +import { Modal, ButtonComponent, setIcon } from 'obsidian'; export interface CompletionResult { /** Title shown in the modal header */ title: string; - /** Icon emoji for the operation type */ + /** Lucide icon name for the operation type */ icon?: string; /** Total number of items processed */ total: number; @@ -34,28 +34,30 @@ export class CompletionModal extends Modal { contentEl.addClass('mdb-completion-modal'); const r = this.result; - const icon = r.icon ?? '✅'; + const icon = r.icon ?? 'check-circle'; const allSuccess = r.errors === 0; // Header - const header = contentEl.createEl('div', { cls: 'mdb-completion-header' }); - header.createEl('span', { cls: 'mdb-completion-icon', text: allSuccess ? icon : '⚠️' }); + const header = contentEl.createEl('div', { cls: 'mdb-completion-header media-db-list-item-flex' }); + header.style.alignItems = 'center'; + const iconEl = header.createEl('span', { cls: 'mdb-completion-icon' }); + setIcon(iconEl, allSuccess ? icon : 'alert-triangle'); header.createEl('h2', { cls: 'mdb-completion-title', text: r.title }); // Stats const stats = contentEl.createEl('div', { cls: 'mdb-completion-stats' }); - this.addStatRow(stats, '📄 Total', `${r.total}`); - this.addStatRow(stats, '✅ Successful', `${r.success}`, 'success'); - this.addStatRow(stats, '❌ Errors', `${r.errors}`, r.errors > 0 ? 'error' : undefined); + this.addStatRow(stats, 'file-text', 'Total', `${r.total}`); + this.addStatRow(stats, 'check-circle', 'Successful', `${r.success}`, 'success'); + this.addStatRow(stats, 'x-circle', 'Errors', `${r.errors}`, r.errors > 0 ? 'error' : undefined); if (r.skipped !== undefined) { - this.addStatRow(stats, '⏭️ Skipped', `${r.skipped}`, 'skipped'); + this.addStatRow(stats, 'skip-forward', 'Skipped', `${r.skipped}`, 'skipped'); } if (r.elapsedMs !== undefined) { const secs = (r.elapsedMs / 1000).toFixed(1); - this.addStatRow(stats, '⏱️ Duration', `${secs}s`); + this.addStatRow(stats, 'clock', 'Duration', `${secs}s`); } // Notes @@ -78,8 +80,11 @@ export class CompletionModal extends Modal { this.contentEl.empty(); } - private addStatRow(container: HTMLElement, label: string, value: string, cls?: string): void { - const row = container.createEl('div', { cls: 'mdb-completion-row' }); + private addStatRow(container: HTMLElement, iconIcon: string, label: string, value: string, cls?: string): void { + const row = container.createEl('div', { cls: 'mdb-completion-row media-db-list-item-flex' }); + row.style.alignItems = 'center'; + const iconEl = row.createEl('span', { cls: 'mdb-completion-row-icon' }); + setIcon(iconEl, iconIcon); row.createEl('span', { cls: 'mdb-completion-label', text: label }); row.createEl('span', { cls: `mdb-completion-value${cls ? ' mdb-stat-' + cls : ''}`, text: value }); } diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 39821aba..cd646e34 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -47,10 +47,7 @@ export class MediaDbSearchResultModal extends SelectModal { // Renders each suggestion item. renderElement(item: MediaTypeModel, el: HTMLElement): void { - el.addClass('media-db-plugin-select-element-flex'); - el.style.display = 'flex'; - el.style.gap = '8px'; - el.style.alignItems = 'flex-start'; + el.addClass('media-db-list-item-flex'); const thumb = el.createDiv({ cls: 'media-db-plugin-select-thumb' }); diff --git a/src/modals/MediaDbSeasonSelectModal.ts b/src/modals/MediaDbSeasonSelectModal.ts index d7519a1d..c56fc7d3 100644 --- a/src/modals/MediaDbSeasonSelectModal.ts +++ b/src/modals/MediaDbSeasonSelectModal.ts @@ -24,20 +24,13 @@ export class MediaDbSeasonSelectModal extends SelectModal): string { - const title = albumTitle.trim(); - const artistsRaw = songMeta.artists; - const artists = Array.isArray(artistsRaw) - ? artistsRaw.filter((a): a is string => typeof a === 'string') - : []; - const year = coerceYear(songMeta.year); - const releaseModel = new MusicReleaseModel({ - type: 'musicRelease', - title, - englishTitle: title, - year, - releaseDate: '', - dataSource: '', - url: '', - id: '', - image: '', - artists, - genres: [], - subType: 'album', - language: '', - rating: 0, - userData: { personalRating: 0 }, - }); - const linkTarget = this.plugin.mediaTypeManager.getFileName(releaseModel); - if (linkTarget === title) { - return `[[${linkTarget}]]`; - } - return `[[${linkTarget}|${title}]]`; - } + // --- Helper logic removed to src/utils/musicFormatHelper.ts per developer feedback --- } diff --git a/src/styles.css b/src/styles.css index caf54cc3..2fd75753 100644 --- a/src/styles.css +++ b/src/styles.css @@ -533,3 +533,19 @@ small.media-db-plugin-list-text { justify-content: flex-end; padding-top: 12px; } + +/* ── Modals Inline Style Replacements ─────────────────────────────── */ + +.media-db-list-item-flex { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.media-db-list-item-thumb { + background: var(--background-modifier-hover); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/utils/AutoTrackerHelper.ts b/src/utils/AutoTrackerHelper.ts index 28628a07..24633540 100644 --- a/src/utils/AutoTrackerHelper.ts +++ b/src/utils/AutoTrackerHelper.ts @@ -78,7 +78,7 @@ export class AutoTrackerHelper { new CompletionModal(this.plugin.app, { title: 'Auto Tracker Complete', - icon: '🎯', + icon: 'target', total: filesToUpdate.length, success: successCount, errors: failCount, diff --git a/src/utils/BulkImportHelper.ts b/src/utils/BulkImportHelper.ts index eeda4cae..85e4e730 100644 --- a/src/utils/BulkImportHelper.ts +++ b/src/utils/BulkImportHelper.ts @@ -89,7 +89,7 @@ export class BulkImportHelper { const total = successCount + erroredFiles.length; new CompletionModal(this.plugin.app, { title: 'Bulk Import Complete', - icon: '📥', + icon: 'folder-down', total, success: successCount, errors: erroredFiles.filter(e => !e.canceled).length, diff --git a/src/utils/BulkUpdateHelper.ts b/src/utils/BulkUpdateHelper.ts index 5396f327..39002ccb 100644 --- a/src/utils/BulkUpdateHelper.ts +++ b/src/utils/BulkUpdateHelper.ts @@ -53,7 +53,7 @@ export class BulkUpdateHelper { new CompletionModal(this.plugin.app, { title: 'Bulk Update Complete', - icon: '🔄', + icon: 'refresh-cw', total: mediaFiles.length, success: successCount, errors: failCount, diff --git a/src/utils/musicFormatHelper.ts b/src/utils/musicFormatHelper.ts new file mode 100644 index 00000000..53813a25 --- /dev/null +++ b/src/utils/musicFormatHelper.ts @@ -0,0 +1,63 @@ +import type MediaDbPlugin from '../main'; +import { ArtistModel } from '../models/ArtistModel'; +import { MusicReleaseModel } from '../models/MusicReleaseModel'; +import { coerceYear } from './Utils'; + +export function artistTitleWikilink(artistTitle: string, plugin: MediaDbPlugin): string { + const title = artistTitle.trim(); + const artistModel = new ArtistModel({ + type: 'artist', + title, + englishTitle: title, + year: 0, + beginYear: '', + releaseDate: '', + dataSource: '', + url: '', + id: '', + country: '', + disambiguation: '', + isni: '', + genres: [], + image: '', + officialWebsite: '', + subType: 'artist', + userData: { personalRating: 0 }, + }); + const linkTarget = plugin.mediaTypeManager.getFileName(artistModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; +} + +export function songAlbumTitleWikilink(albumTitle: string, songMeta: Record, plugin: MediaDbPlugin): string { + const title = albumTitle.trim(); + const artistsRaw = songMeta.artists; + const artists = Array.isArray(artistsRaw) + ? artistsRaw.filter((a): a is string => typeof a === 'string') + : []; + const year = coerceYear(songMeta.year); + const releaseModel = new MusicReleaseModel({ + type: 'musicRelease', + title, + englishTitle: title, + year, + releaseDate: '', + dataSource: '', + url: '', + id: '', + image: '', + artists, + genres: [], + subType: 'album', + language: '', + rating: 0, + userData: { personalRating: 0 }, + }); + const linkTarget = plugin.mediaTypeManager.getFileName(releaseModel); + if (linkTarget === title) { + return `[[${linkTarget}]]`; + } + return `[[${linkTarget}|${title}]]`; +} diff --git a/test/genius-lyrics.test.ts b/test/genius-lyrics.test.ts index d7b1c02e..4a76bee2 100644 --- a/test/genius-lyrics.test.ts +++ b/test/genius-lyrics.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test'; -import { extractLyricsFromGeniusHtml } from '../src/api/geniusLyricsExtract'; +import { extractLyricsFromGeniusHtml } from '../src/api/helpers/geniusLyricsExtract'; describe('extractLyricsFromGeniusHtml', () => { test('keeps all lines when lyrics use nested divs (balanced extraction)', () => { From abaf7a565ef0bcfeb913d464e7645a8dae7670fb Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:33:55 +0300 Subject: [PATCH 53/60] Add files via upload --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db5191d6..d5eaf3d8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "main.js", "scripts": { "dev": "vite build --watch --mode development", - "build": "npm run tsc && vite build --mode production", + "build": "bun run tsc && vite build --mode production", "tsc": "tsc -noEmit -skipLibCheck", "test": "npm test", "test:log": "LOG_TESTS=true npm test", From c781ac0818e7268d7100154aa140d477716b4c59 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:34:36 +0300 Subject: [PATCH 54/60] Add files via upload --- src/styles.css | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/styles.css b/src/styles.css index 2fd75753..572b146a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -486,23 +486,30 @@ small.media-db-plugin-list-text { } .mdb-completion-row { - display: flex; + display: grid; + grid-template-columns: 32px 1fr 32px; align-items: center; - justify-content: space-between; - gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--background-modifier-border); } +.mdb-completion-row-icon { + display: flex; + align-items: center; + color: var(--text-muted); +} + .mdb-completion-label { color: var(--text-muted); font-size: 0.92em; + text-align: center; } .mdb-completion-value { font-weight: 600; color: var(--text-normal); font-size: 0.95em; + text-align: right; } .mdb-stat-success { From 95418b5d8d177a8d2360b6a9f78a01f4c2f509b0 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:35:14 +0300 Subject: [PATCH 55/60] Add files via upload --- src/modals/CompletionModal.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modals/CompletionModal.ts b/src/modals/CompletionModal.ts index 362d192b..f9a717f1 100644 --- a/src/modals/CompletionModal.ts +++ b/src/modals/CompletionModal.ts @@ -81,8 +81,7 @@ export class CompletionModal extends Modal { } private addStatRow(container: HTMLElement, iconIcon: string, label: string, value: string, cls?: string): void { - const row = container.createEl('div', { cls: 'mdb-completion-row media-db-list-item-flex' }); - row.style.alignItems = 'center'; + const row = container.createEl('div', { cls: 'mdb-completion-row' }); const iconEl = row.createEl('span', { cls: 'mdb-completion-row-icon' }); setIcon(iconEl, iconIcon); row.createEl('span', { cls: 'mdb-completion-label', text: label }); From 4dddc67b0ecaef1ebd2eb43e7b42adf8cdf966a8 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:03:34 +0300 Subject: [PATCH 56/60] Delete src/api/geniusLyricsExtract.ts --- src/api/geniusLyricsExtract.ts | 89 ---------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 src/api/geniusLyricsExtract.ts diff --git a/src/api/geniusLyricsExtract.ts b/src/api/geniusLyricsExtract.ts deleted file mode 100644 index ecdc178a..00000000 --- a/src/api/geniusLyricsExtract.ts +++ /dev/null @@ -1,89 +0,0 @@ -const LYRICS_CONTAINER_OPEN_RE = /]*\bdata-lyrics-container\s*=\s*(?:"true"|'true'|true)[^>]*>/gi; - -function stripHtmlToPlainLyrics(fragment: string): string { - return fragment - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n') - .replace(/<[^>]+>/g, '') - .replace(/\n{3,}/g, '\n\n') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .trim(); -} - -/** Parses nested
blocks; the naive `*?
` regex stops at the first inner close tag. */ -function extractBalancedDivInnerHtml(html: string, contentStart: number): string { - let depth = 1; - let i = contentStart; - const openRe = //gi; - while (depth > 0) { - openRe.lastIndex = i; - closeRe.lastIndex = i; - const om = openRe.exec(html); - const cm = closeRe.exec(html); - if (!cm) { - break; - } - const oIdx = om ? om.index : Number.POSITIVE_INFINITY; - const cIdx = cm.index; - if (om && oIdx < cIdx) { - depth++; - i = om.index + om[0].length; - } else { - depth--; - if (depth === 0) { - return html.slice(contentStart, cIdx); - } - i = cm.index + cm[0].length; - } - } - return ''; -} - -function collectLyricsContainersRegex(html: string): string[] { - const chunks: string[] = []; - let m: RegExpExecArray | null; - LYRICS_CONTAINER_OPEN_RE.lastIndex = 0; - while ((m = LYRICS_CONTAINER_OPEN_RE.exec(html)) !== null) { - const inner = extractBalancedDivInnerHtml(html, m.index + m[0].length); - if (inner) { - chunks.push(inner); - } - } - return chunks; -} - -function extractOneContainerPlain(el: Element): string { - const clone = el.cloneNode(true) as Element; - clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(node => node.remove()); - return stripHtmlToPlainLyrics(clone.innerHTML); -} - -export function extractLyricsFromGeniusHtml(html: string): string { - let chunks: string[] = []; - try { - const doc = new DOMParser().parseFromString(html, 'text/html'); - doc.querySelectorAll('[data-lyrics-container="true"]').forEach(c => { - const plain = extractOneContainerPlain(c); - if (plain) { - chunks.push(plain); - } - }); - } catch { - chunks = []; - } - - if (chunks.length === 0) { - return ''; - } - - return chunks - .join('\n\n') - .replace(/\n{3,}/g, '\n\n') - .trim(); -} From b64bae90e913a45787ea18521ffc30aa8e000c7e Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:08:34 +0300 Subject: [PATCH 57/60] Add files via upload --- src/settings/PropertyMapper.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 82aae303..73e1219d 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -4,7 +4,6 @@ import { MusicReleaseModel } from '../models/MusicReleaseModel'; import { MediaType } from '../utils/MediaType'; import { noteTypeValueForMedia, resolveMetadataTypeToMediaType } from '../utils/noteTypeSettings'; import { coerceYear } from '../utils/Utils'; -import { artistTitleWikilink, songAlbumTitleWikilink } from '../utils/musicFormatHelper'; import { PropertyMappingOption } from './PropertyMapping'; export class PropertyMapper { @@ -56,31 +55,13 @@ export class PropertyMapper { let finalValue = value; if (propertyMapping.wikilink) { - const useArtistFileNameForArtists = - propertyMapping.property === 'artists' && - (internalMediaType === MediaType.Song || internalMediaType === MediaType.MusicRelease); - const useMusicReleaseFileNameForAlbumTitle = - propertyMapping.property === 'albumTitle' && internalMediaType === MediaType.Song; - if (typeof value === 'string') { - if (useArtistFileNameForArtists) { - finalValue = artistTitleWikilink(value, this.plugin); - } else if (useMusicReleaseFileNameForAlbumTitle) { - finalValue = songAlbumTitleWikilink(value, obj, this.plugin); - } else { - finalValue = `[[${value}]]`; - } + finalValue = `[[${value}]]`; } else if (Array.isArray(value)) { finalValue = value.map((v: unknown) => { if (typeof v !== 'string') { return v; } - if (useArtistFileNameForArtists) { - return artistTitleWikilink(v, this.plugin); - } - if (useMusicReleaseFileNameForAlbumTitle) { - return songAlbumTitleWikilink(v, obj, this.plugin); - } return `[[${v}]]`; }); } From 3504b46f4aafdeab6932a948d634e684e6329502 Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:32:58 +0300 Subject: [PATCH 58/60] fix - Updated BulkUpdateConfirmModal to accept custom titles and descriptions - Added context-specific warning text for AutoTracker syncs to avoid confusing generic bulk update warnings --- src/main.ts | 22 ++++++++++++++++------ src/modals/BulkUpdateConfirmModal.ts | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main.ts b/src/main.ts index ee23a4e4..944c7d50 100644 --- a/src/main.ts +++ b/src/main.ts @@ -157,9 +157,14 @@ export default class MediaDbPlugin extends Plugin { .setTitle('Start Auto-Tracker in Folder') .setIcon('sync') .onClick(() => { - new BulkUpdateConfirmModal(this.app, (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); - }).open(); + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate, file); + }, + 'Auto Tracker Sync', + 'You are about to scan and automatically update Airing/Released status for tracked media in this folder.' + ).open(); }), ); sub.addItem((subItem: any) => @@ -1426,9 +1431,14 @@ export default class MediaDbPlugin extends Plugin { if (this.autoTrackerHelper.isScanning) { new Notice('Auto-Tracker is currently syncing in the background.'); } else { - new BulkUpdateConfirmModal(this.app, (silentUpdate: boolean) => { - this.autoTrackerHelper.startBackgroundScan(silentUpdate); - }).open(); + new BulkUpdateConfirmModal( + this.app, + (silentUpdate: boolean) => { + this.autoTrackerHelper.startBackgroundScan(silentUpdate); + }, + 'Auto Tracker Sync', + 'You are about to scan and automatically update Airing/Released status for tracked media across your vault.' + ).open(); } }); this._ribbonEl.addClass('obsidian-media-db-plugin-ribbon-class'); diff --git a/src/modals/BulkUpdateConfirmModal.ts b/src/modals/BulkUpdateConfirmModal.ts index 7afb18e8..4a621100 100644 --- a/src/modals/BulkUpdateConfirmModal.ts +++ b/src/modals/BulkUpdateConfirmModal.ts @@ -4,16 +4,25 @@ import { Modal, Setting } from 'obsidian'; export class BulkUpdateConfirmModal extends Modal { onSubmit: (silent: boolean) => void; silentUpdate: boolean = false; + customTitle: string; + customDesc: string; - constructor(app: App, onSubmit: (silent: boolean) => void) { + constructor( + app: App, + onSubmit: (silent: boolean) => void, + customTitle: string = 'Bulk Update Metadata', + customDesc: string = 'You are about to scan and update metadata for notes in this folder.' + ) { super(app); this.onSubmit = onSubmit; + this.customTitle = customTitle; + this.customDesc = customDesc; } onOpen() { const { contentEl } = this; - contentEl.createEl('h2', { text: 'Bulk Update Metadata' }); - contentEl.createEl('p', { text: 'You are about to scan and update metadata for notes in this folder.' }); + contentEl.createEl('h2', { text: this.customTitle }); + contentEl.createEl('p', { text: this.customDesc }); new Setting(contentEl) .setName('Update Silently (No Confirmations)') From 6e9e6f06719b8670f161535a0fbedcbb7f28f07b Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Sun, 5 Apr 2026 02:32:52 +0300 Subject: [PATCH 59/60] Fix: silent update toggle for bulk tools - Corrected the parameter order in updateNote() for bulk helpers. - Fixed silent toggle (overwrite) in the 5th parameter position. - Disabled opening new tabs (openNoteFinal) during bulk runs. - Updated BulkUpdate, BulkRecreate, and AutoTracker helpers. --- src/utils/AutoTrackerHelper.ts | 2 +- src/utils/BulkRecreateHelper.ts | 2 +- src/utils/BulkUpdateHelper.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/AutoTrackerHelper.ts b/src/utils/AutoTrackerHelper.ts index 24633540..c7780594 100644 --- a/src/utils/AutoTrackerHelper.ts +++ b/src/utils/AutoTrackerHelper.ts @@ -57,7 +57,7 @@ export class AutoTrackerHelper { for (const file of filesToUpdate) { try { - await this.plugin.updateNote(file, true, false, silent); + await this.plugin.updateNote(file, true, false, false, silent); successCount++; } catch (e) { console.warn(`MDB Tracker | Failed to auto-update ${file.path}: `, e); diff --git a/src/utils/BulkRecreateHelper.ts b/src/utils/BulkRecreateHelper.ts index 24844da9..1bbd7987 100644 --- a/src/utils/BulkRecreateHelper.ts +++ b/src/utils/BulkRecreateHelper.ts @@ -37,7 +37,7 @@ export class BulkRecreateHelper { for (const file of mediaFiles) { try { - await this.plugin.updateNote(file, onlyMetadata, false, silent); + await this.plugin.updateNote(file, onlyMetadata, false, false, silent); successCount++; } catch (e) { console.error(`MDB | Failed to bulk recreate ${file.path}: `, e); diff --git a/src/utils/BulkUpdateHelper.ts b/src/utils/BulkUpdateHelper.ts index 39002ccb..153b77a2 100644 --- a/src/utils/BulkUpdateHelper.ts +++ b/src/utils/BulkUpdateHelper.ts @@ -33,7 +33,7 @@ export class BulkUpdateHelper { for (const file of mediaFiles) { try { - await this.plugin.updateNote(file, true, false, silent); + await this.plugin.updateNote(file, true, false, false, silent); successCount++; } catch (e) { console.error(`MDB | Failed to bulk update ${file.path}: `, e); From 77f337edabee8ffca9de47a8bf23eb234b185a7a Mon Sep 17 00:00:00 2001 From: Mehmet Saim Deniz <57042756+saimdeniz@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:02:28 +0300 Subject: [PATCH 60/60] fix recreate function --- src/main.ts | 24 ++++++++++++++++++++++-- src/styles.css | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 944c7d50..bde78701 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1115,12 +1115,13 @@ export default class MediaDbPlugin extends Plugin { mediaTypeModel.type = noteTypeValueForMedia(this.settings, mediaTypeModel.getMediaType()); let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); + const originalTemplateText = template; let fileMetadata: Record = this.modelPropertyMapper.convertObject(this.metadataRecordForNewNote(mediaTypeModel)); let fileContent = ''; template = options.attachTemplate ? template : ''; - ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile, options.preservePropertyOrder)); + ({ fileMetadata, fileContent } = await this.attachFile(fileMetadata, fileContent, options.attachFile, options.preservePropertyOrder, originalTemplateText)); ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); // --- Global Wiki-Link Post-Processing (for Custom/Manual Properties) --- @@ -1245,7 +1246,7 @@ export default class MediaDbPlugin extends Plugin { return allTags.map(t => String(t).trim()).filter(t => t && !autoTagValues.has(t.toLowerCase()) && !t.toLowerCase().startsWith('mediadb/')); } - async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile, preservePropertyOrder?: boolean): Promise<{ fileMetadata: Metadata; fileContent: string }> { + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile, preservePropertyOrder?: boolean, templateStr?: string): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!fileToAttach) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } @@ -1273,6 +1274,25 @@ export default class MediaDbPlugin extends Plugin { for (const key of Object.keys(fileMetadata)) { orderedMetadata[key] = fileMetadata[key]; } + + // Smart Sort: extract predefined order from template (if available) + let templateMetadata: Record = {}; + const templateKeys: string[] = []; + if (templateStr) { + templateMetadata = this.getMetaDataFromFileContent(templateStr); + templateKeys.push(...Object.keys(templateMetadata)); + } + + // Add properties matching the template order first + for (const tKey of templateKeys) { + if (tKey in attachFileMetadata && !(tKey in orderedMetadata)) { + orderedMetadata[tKey] = attachFileMetadata[tKey]; + } else if (!(tKey in attachFileMetadata) && !(tKey in orderedMetadata)) { + orderedMetadata[tKey] = templateMetadata[tKey]; + } + } + + // Then add any remaining unexpected properties (at the very bottom) for (const [key, value] of Object.entries(attachFileMetadata)) { if (!(key in orderedMetadata)) { orderedMetadata[key] = value; diff --git a/src/styles.css b/src/styles.css index 572b146a..26baede5 100644 --- a/src/styles.css +++ b/src/styles.css @@ -487,7 +487,7 @@ small.media-db-plugin-list-text { .mdb-completion-row { display: grid; - grid-template-columns: 32px 1fr 32px; + grid-template-columns: 32px 1fr auto; align-items: center; padding: 7px 0; border-bottom: 1px solid var(--background-modifier-border);